Adds computeVariantPrice(), a verbatim port of the live formula at henza_server/app/Models/ProductVariant.php:183-190, with the per-customer making-charge offset as an explicit input defaulting to 0. The three superseded formulas commented out above it apply VAT to the whole price and are deliberately not ported (D4). 11 unit tests assert against hand calculations derived from the PHP source rather than from the implementation, covering: with and without the offset, VAT on the non-metal portion only, floor-to-nearest-1000, the extraPercent default of 7, and the equivalence between the app folding the offset into percent_pay and the server deriving it. 18 tests pass overall. Also adds the D0 name-split review report script (read-only) and two investigations the owner asked for: - SMS: the reference uses the Trez panel (smspanel.trez.ir, Sms.php:69,97) with production credentials committed to git (SEC-12 — rotate them). All 12 message templates catalogued; env var names proposed; nothing implemented pending approval. - MON-08: henza_pwa computes percent_pay, discount and rial_pay rather than asking the user (cart.vue:170-179, Mixins/product/index.ts:69-75), so Phase 2 item #1 is to derive them server-side and ignore the client's values. rash_weight stays read from the DB as a recorded deviation. Records the owner's two new standing rules: migrations only through the standard Prisma workflow, and every data deletion logged in CHANGELOG. [ref: henza_server/app/Models/{ProductVariant,Sms}.php, henza_pwa/src/views/cart.vue] Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
209 lines
7.3 KiB
TypeScript
209 lines
7.3 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|