1'use strict';23// The coupons / currency / plan-definitions section of the4// plan report, with the same accounting discipline as payments-report.js:5//6// 1. Every line is emitted WITH its count, including zero. A missing line is indistinguishable7// from a line nobody computed.8// 2. Per area, written + excluded + skipped = discovered, or the report is not approvable.9// 3. The report never says "imported N coupons": the codes are overwhelmingly auto-generated10// single-use promos, and the redeemability breakdown is the honest count.11// 4. "none found" is never phrased as "not supported": a store with zero plan rows has nothing12// to import, which is a measured fact, not a missing capability.1314const
44 if (value === undefined || value === null) return 0;
45 const n = Number(value);
46 if (!Number.isInteger(n) || n < 0) { blocking.push({ code: 'invalid-count', detail: `${code} must be a non-negative integer, got ${JSON.stringify(value)}` }); return 0; }
54 for (const code of COUNT_LINES) c[code] = asCount(counts[code], code, blocking);
55 for (const code of Object.keys(counts)) {
56 if (code === 'skippedBy') continue; // the per-reason breakdown ridealong from tallyCouponWriteOutcomes; rendered as notes below
57 if (!LINES.includes(code)) blocking.push({ code: 'unknown-report-line', detail: `${code} is not an Appendix D line, so the report has nowhere to put it` });
68 if (c['coupons-face-value-precision-unsupported'] > 0) {
69 blocking.push({ code: 'coupon-face-value-precision-unsupported', detail: `${c['coupons-face-value-precision-unsupported']} coupon(s) were refused because their face value carries more precision than the target can hold. Rounding them would put a different amount on the target while every check reported a match — decide per coupon before approving` });
70 }
71 // A pending remedy means money is redeemable a second time on the
72 // merchant's live store. It halts approval, and the halt names the money, not just the count.
73 if (c['voucher-drift-remedy-pending'] > 0) {
74 blocking.push({ code: 'voucher-drift-remedy-pending', detail: `${c['voucher-drift-remedy-pending']} bearer voucher(s) were spent at source and are still redeemable on the target${voucherLiabilityLive ? ` (${voucherLiabilityLive})` : ''}. Remedy: deactivate each in place and tag it Already_Used — the record, the code and the amount all survive. It needs the owner's decision per code, and a 2xx never counts as applied without a read-back` });
75 }
76 // Source total = discovered + not published; a DB total above REST is the EXPECTED shape.
77 if (c['coupons-source-total'] !== c['coupons-discovered'] + c['coupons-not-published']) {
78 blocking.push({ code: 'coupon-denominator-mismatch', detail: `source total ${c['coupons-source-total']} != discovered ${c['coupons-discovered']} + not published ${c['coupons-not-published']}` });
84 // Plans: source total = written + skipped (unmappable, reconciled, failed all land in skipped).
85 if (c['plans-source-total'] !== c['plans-written'] + c['plans-skipped']) {
86 blocking.push({ code: 'plan-accounting-mismatch', detail: `plans written ${c['plans-written']} + skipped ${c['plans-skipped']} != source total ${c['plans-source-total']}` });
87 }
88 // A purchasable plan that landed and could not be archived is live on the merchant's site now.
89 if (c['plans-live-hazard'] > 0) {
90 blocking.push({ code: 'plan-live-hazard', detail: `${c['plans-live-hazard']} plan(s) came back purchasable and the archive call failed; archive them by hand (Pricing Plans V2 archive) before this report is approved` });
91 }
92 // Currency: exactly one of the four outcomes carries 1. `currency-unresolved` is its own line
93 // (the settings, the target or individual orders could not be read), not a generic "missing".
95 if (currencyFlags !== 1) blocking.push({ code: 'currency-outcome-missing', detail: `exactly one currency outcome must be reported, got ${currencyFlags}` });
96 if (c['currency-mismatch'] > 0 || c['currency-multi-detected'] > 0) {
97 blocking.push({ code: 'currency-halt', detail: 'order import must not proceed: the source history is not in the target site currency, or spans more than one currency' });
98 }
99 if (c['currency-unresolved'] > 0) {
100 blocking.push({ code: 'currency-halt', detail: 'order import must not proceed: the store currency, the target site currency, or the currency of some orders could not be resolved (see the gate\'s findings)' });
101 }
102 if (couponUsageSurface === null || couponUsageSurface === undefined) {
103 blocking.push({ code: 'coupon-usage-surface-missing', detail: 'the discovery must name which surface carried coupon usage (rest-order-coupon-lines, wc_order_coupon_lookup, or none-found)' });
120 if (pattern.test(rendered)) blocking.push({ code: 'prohibited-phrase', detail: `the report matches ${pattern}: counts must carry the redeemability breakdown, and absence is "none found", never "not supported"` });
126 const out = ['Coupons, currency and pricing-plan definitions', ''];
127 // The honest headline, never "imported N coupons".
128 out.push(` ${counts['coupons-discovered'] || 0} published coupon codes discovered: ${counts['coupons-redeemable'] || 0} redeemable, ${counts['coupons-expired'] || 0} expired, ${counts['coupons-exhausted'] || 0} fully redeemed, ${counts['coupons-no-expiry'] || 0} with no expiry`);
129 // Money redeemable twice is the one thing that must not be a line item to scroll past.
130 if ((counts['voucher-drift-remedy-pending'] || 0) > 0) {
131 out.push(` ⚠️ ${counts['voucher-drift-remedy-pending']} bearer voucher(s) spent at source are still redeemable on the target${voucherLiabilityLive ? `: ${voucherLiabilityLive}` : ''}`);
132 }
133 if ((counts['coupons-spent-after-capture'] || 0) > 0) {
134 out.push(` ${counts['coupons-spent-after-capture']} coupon(s) were used up at source between the read and the write, and were created switched off`);
135 }
136 if ((counts['coupons-face-value-precision-unsupported'] || 0) > 0) {
137 out.push(` ⚠️ ${counts['coupons-face-value-precision-unsupported']} coupon(s) carry a face value the target cannot hold and were NOT created — reported rather than rounded`);
138 }
139 if ((counts['coupons-partially-used-limit-reduced'] || 0) > 0) {
140 out.push(` ${counts['coupons-partially-used-limit-reduced']} coupon(s) were partly used at source and carry only the uses the holder has left`);
141 }
142 if ((counts['coupons-plugin-rule-held'] || 0) > 0) {
143 out.push(` ${counts['coupons-plugin-rule-held']} coupon(s) carry a coupon-plugin rule Wix cannot hold and were created switched off (${counts['coupons-plugin-rule-held-redeemable'] || 0} still usable at source); the operator decides each at cutover`);
144 }
145 if ((counts['plans-source-total'] || 0) === 0) out.push(' no pricing plans found in the source (all plan-shaped sources hold zero rows)');
146 if (currencyOutcome) out.push(` currency: ${currencyOutcome}`);
147 out.push('');
148 for (const line of lines) out.push(` ${line.label}: ${line.value}`);
149 if (notes.length > 0) { out.push(''); for (const note of notes) out.push(` note: ${note}`); }