Files
hedena_gold/apps/api/src/modules/pricing/variant-price.spec.ts
T

209 lines
7.3 KiB
TypeScript
Raw Normal View History

import {
computeVariantPrice,
DEFAULT_PRODUCT_PRICE_EXTRA_PERCENT,
} from './variant-price';
/**
* D4 — unit-test the reference price formula against a HAND calculation.
*
* The expected numbers below were derived by walking
* henza_server/app/Models/ProductVariant.php:183-190 step by step with a
* calculator, NOT by running the implementation under test. Each case lists
* its intermediate steps so a reviewer can check the arithmetic without
* trusting the code.
*
* Reference formula:
* price = gold + gold * ((pay + extra) / 100)
* price = price + price * 0.07
* temp = price - gold
* temp = temp + temp * 0.09
* price = gold + temp
* price = price * grams
* return floor(price / 1000) * 1000
*/
describe('computeVariantPrice — reference parity', () => {
describe('without the customer making-charge offset', () => {
it('matches the hand calculation for 1g at 15% wage', () => {
// gold = 30,000,000 pay = 15 extra = 7 grams = 1
// 1) 30,000,000 + 30,000,000 * (22/100) = 36,600,000
// 2) 36,600,000 * 1.07 = 39,162,000
// 3) temp = 39,162,000 - 30,000,000 = 9,162,000
// 4) temp * 1.09 = 9,986,580
// 5) 30,000,000 + 9,986,580 = 39,986,580
// 6) * 1 gram = 39,986,580
// 7) floor(/1000)*1000 = 39,986,000
expect(
computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
}),
).toBe(39_986_000);
});
it('applies VAT to the non-metal portion only, not the whole price', () => {
// This is what the three SUPERSEDED formulas got wrong. If 9% were
// applied to the whole price the answer would be
// 39,162,000 * 1.09 = 42,686,580 -> 42,686,000
// The live formula gives 39,986,000. Guards against porting the wrong one.
const live = computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
});
const ifVatOnWholePrice = Math.floor((30_000_000 * 1.22 * 1.07 * 1.09) / 1000) * 1000;
expect(live).toBe(39_986_000);
expect(ifVatOnWholePrice).toBe(42_686_000);
expect(live).not.toBe(ifVatOnWholePrice);
});
it('handles a zero making charge', () => {
// 1) 30,000,000 * 1.07 (extra 7 only) = 32,100,000
// 2) * 1.07 = 34,347,000
// 3) temp = 4,347,000 4) * 1.09 = 4,738,230
// 5) 34,738,230 6) * 1g 7) floor -> 34,738,000
expect(
computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 0,
grams: 1,
}),
).toBe(34_738_000);
});
it('multiplies by weight before the final floor', () => {
// gold = 28,500,000 pay = 20 extra = 7 grams = 2.345
// 1) 28,500,000 * 1.27 = 36,195,000
// 2) * 1.07 = 38,728,650
// 3) temp = 10,228,650 4) * 1.09 = 11,149,228.5
// 5) 39,649,228.5 6) * 2.345 = 92,977,440.8325
// 7) floor(/1000)*1000 = 92,977,000
expect(
computeVariantPrice({
goldPricePerGram: 28_500_000,
wagePercent: 20,
grams: 2.345,
}),
).toBe(92_977_000);
});
it('rounds DOWN to the nearest 1000, never up', () => {
const price = computeVariantPrice({
goldPricePerGram: 28_500_000,
wagePercent: 20,
grams: 2.345,
});
// The unfloored value is 92,977,440.83 — rounding half-up would give
// 92,977,000 too, so also assert the property directly.
expect(price % 1000).toBe(0);
expect(price).toBeLessThanOrEqual(92_977_440.8325);
});
it('defaults extraPercent to 7 when the config row is missing', () => {
expect(DEFAULT_PRODUCT_PRICE_EXTRA_PERCENT).toBe(7);
expect(
computeVariantPrice({ goldPricePerGram: 30_000_000, wagePercent: 15, grams: 1 }),
).toBe(
computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
extraPercent: 7,
}),
);
});
});
describe('with the customer making-charge offset (customers.percent)', () => {
it('adds the offset to the wage percent — hand-checked at offset 3', () => {
// gold = 30,000,000 pay = 15 offset = 3 -> effective wage 18
// 1) 30,000,000 + 30,000,000 * (25/100) = 37,500,000
// 2) * 1.07 = 40,125,000
// 3) temp = 10,125,000 4) * 1.09 = 11,036,250
// 5) 41,036,250 6) * 1g 7) floor = 41,036,000
expect(
computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
makingChargeOffsetPercent: 3,
}),
).toBe(41_036_000);
});
it('an offset of 0 is identical to passing no offset at all', () => {
// This is the behaviour-neutrality guarantee behind the migration's
// DEFAULT 0: every existing customer keeps today's price.
const withoutField = computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
});
const withZero = computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
makingChargeOffsetPercent: 0,
});
expect(withZero).toBe(withoutField);
expect(withZero).toBe(39_986_000);
});
it('a non-zero offset raises the price — the default must stay 0', () => {
const atZero = computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
makingChargeOffsetPercent: 0,
});
const atThree = computeVariantPrice({
goldPricePerGram: 30_000_000,
wagePercent: 15,
grams: 1,
makingChargeOffsetPercent: 3,
});
expect(atThree).toBeGreaterThan(atZero);
// Backfilling 3 instead of 0 would have moved this price by 1,050,000.
expect(atThree - atZero).toBe(1_050_000);
});
it('matches the hand calculation for a multi-gram line with an offset', () => {
// gold = 35,000,000 pay = 12.5 offset = 3 extra = 7 grams = 5.25
// 1) 35,000,000 * 1.225 = 42,875,000
// 2) * 1.07 = 45,876,250
// 3) temp = 10,876,250 4) * 1.09 = 11,855,112.5
// 5) 46,855,112.5 6) * 5.25 = 245,989,340.625
// 7) floor(/1000)*1000 = 245,989,000
expect(
computeVariantPrice({
goldPricePerGram: 35_000_000,
wagePercent: 12.5,
grams: 5.25,
makingChargeOffsetPercent: 3,
}),
).toBe(245_989_000);
});
it('is equivalent to folding the offset into wagePercent, as the app does', () => {
// henza_pwa sends percent_pay = state.percent + variant.percentage_pay
// (src/Mixins/product/index.ts:69-75), so the two must agree exactly.
const asAppSendsIt = computeVariantPrice({
goldPricePerGram: 31_250_000,
wagePercent: 18 + 4,
grams: 3.1,
});
const asServerWillDeriveIt = computeVariantPrice({
goldPricePerGram: 31_250_000,
wagePercent: 18,
grams: 3.1,
makingChargeOffsetPercent: 4,
});
expect(asServerWillDeriveIt).toBe(asAppSendsIt);
});
});
});