Setting the file. One moment.
Payments Outcome Registry · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
lib/ payments-outcome-registry.js
JavaScript · 207 lines · 10 KB
14
15 // Mutually exclusive per-order payment outcomes. Their sum must equal the discovered population,
16 // so a new reason belongs HERE rather than folded into an existing bucket.
17 const PAYMENT_OUTCOMES = Object. freeze ([
18 'payment-written' ,
19 'payment-reconciled-existing' ,
20 'payment-not-written-no-amount' ,
21 'payment-not-written-no-paid-date' ,
22 'payment-not-written-multi-payment' ,
23 'payment-not-written-currency-mismatch' ,
24 'payment-reconciliation-ambiguous' ,
25 'payment-target-read-failed' ,
26 'payment-failed' ,
27 ]);
28
29 // Outcomes a writer returns that are NOT the per-order payment partition: refund results, invoice
30 // results, and internal bookkeeping. Everything a writer can hand back must appear in one of the
31 // three lists, or the report has nowhere to put it.
32 const WRITER_OUTCOMES = Object. freeze ([
33 'refund-written' ,
34 'refund-reconciled-existing' ,
35 'refund-without-payment' ,
36 'refund-still-settling' ,
37 'invoice-preserved' ,
38 'invoice-already-preserved' ,
39 'invoice-number-only' ,
40 'invoice-none-found' ,
41 'invoice-import-failed' ,
42 'invoice-import-not-ready' ,
43 'invoice-import-read-failed' ,
44 // The Media file is READY but the order patch failed; the file id rides back as
45 // pendingMediaFileId so the resume does not import a second copy.
46 'invoice-fields-write-failed' ,
47 'invoice-fields-not-provisioned' ,
48 'invoice-preservation-not-approved' ,
49 'invoice-preservation-declined' ,
50 'invoice-document-url-unsafe' ,
51 // The order-history composite's own outcomes. The ORDER stage has four; a stage that threw after
52 // the order existed is a named partial-failure outcome rather than an exception, so a resumed
53 // run reconciles the order and retries only what failed.
54 'order-imported' ,
55 'order-already-imported' ,
56 'order-import-failed' ,
57 'order-target-read-failed' ,
58 'refund-failed' ,
59 'invoice-failed' ,
60 // The buyer stage -- which member, if any, an imported order was attributed to.
61 // These PARTITION the order population (see BUYER_OUTCOMES); `buyer-not-resolved` is the
62 // composite's answer when no buyer was supplied.
63 'buyer-member-linked' ,
64 'buyer-guest' ,
65 'buyer-contact-only' ,
66 'buyer-member-not-imported' ,
67 'buyer-member-email-mismatch' ,
68 'buyer-not-resolved' ,
69 // Product category attachment (stores-category-attach.js). The identical orthogonal-axis shape
70 // as the order sub-stages above, but for products: a product the entity row already counts as
71 // `imported` can still carry zero of its categories, invisibly, unless this stage is tracked on
72 // its own. The failure this guards against: a 3-argument call site silently dropped
73 // every category-attach write's payload to `{ productId: undefined, categoryIds: undefined }`,
74 // every request almost certainly failed, and a wrapping `catch {}` hid it completely -- a
75 // completion report showed `product: reconciled, mismatch: false` while no product actually
76 // carried its categories live.
77 'category-attach-written' ,
78 'category-attach-reconciled-existing' ,
79 'category-attach-unverified' ,
80 'category-attach-failed' ,
81 // Internal: a target read that carried no mapped payment, and the pre-create verdict.
82 'target-read' ,
83 'create' ,
84 ]);
85
86 // Nested findings — attached to a result rather than returned as its outcome. They count too: an
87 // over-length link is a loss the merchant should see even though the document was preserved.
88 const FINDING_CODES = Object. freeze ([
89 'payment-status-conflict' ,
90 'paid-date-disagreement' ,
91 'paid-date-timezone-assumed' ,
92 'paid-date-unparseable' ,
93 'reference-without-paid-date' ,
94 'no-transaction-reference' ,
95 'ambiguous-transaction-reference' ,
96 'incompatible-gateway-metadata' ,
97 'unverified-gateway-key' ,
98 'reference-over-length' ,
99 'method-name-over-length' ,
100 'card-display-over-length' ,
101 'unclassified-method' ,
102 'unmapped-gateway-family' ,
103 'refund-weak-signal' ,
104 'refund-date-not-preserved' ,
105 'refund-line-detail-not-preserved' ,
106 'invoice-link-over-length' ,
107 // Live-verified 2026-10-27: Update Order silently drops an over-maxLength value, so the writer
108 // checks the number itself, and reports any key the PATCH response does not carry back.
109 'invoice-number-over-length' ,
110 'invoice-field-dropped' ,
111 'saved-cards-not-transferred' ,
112 'redacted-source-values' ,
113 // Order-history composite: the order's id came from a crosswalk row rather than being derived from the source
114 // id -- an order imported before deterministic ids, or by another tool. Counted so a store
115 // where every row says this is visibly an import from before deterministic ids rather than a derivation bug.
116 'order-id-from-crosswalk' ,
117 // Order-history composite: Import Order returned an id other than the one supplied. The target's id is adopted
118 // and persisted so re-runs converge; the finding says the identity model did not hold here.
119 'order-id-not-derived' ,
120 // Order-history composite: a crosswalk row pointed at an order the target no longer has (a cleared migration),
121 // so the order was imported again under the derived id and the row replaced.
122 'order-reimported-after-deletion' ,
123 // Order-history composite: a crosswalk row pointed at a missing order while the DERIVED-id order exists; the
124 // existing order was adopted and the row replaced, nothing imported.
125 'order-crosswalk-row-stale' ,
126 // Order-history composite: a remembered Media file came from a different source URL and was not resumed.
127 'invoice-pending-file-superseded' ,
128 // Buyer stage: crosswalk values that could not be used as ids.
129 'buyer-member-id-not-guid' ,
130 'buyer-contact-id-not-guid' ,
131 // Import Order refused an order on a contact field its rejection NAMED, so that
132 // field was removed and the order sent once more. The row names
133 // the field and the reason, never the value. Registering a code is what gives it a report line
134 // (REPORTABLE feeds ADDITIONAL_COUNTS), including a zero line on a clean run. The `order-`/
135 // `buyer-` prefixes are load-bearing: payments-report-lines-test.js scans writer source for
136 // exactly those prefixes, and a code it cannot see is a code nothing can count.
137 'order-contact-email-dropped' ,
138 'order-contact-phone-dropped' ,
139 // A WARNING, not a loss: a local check dislikes a source email or phone, which is
140 // kept and sent unchanged. Reported as its own line; never summed with the drops above.
141 'order-contact-email-suspect' ,
142 'order-contact-phone-suspect' ,
143 // The order carried an email `normalizeEmail` refused, so a named customer became
144 // an anonymous guest. A finding, NOT an outcome: the buyer partition is unchanged.
145 'buyer-email-unusable' ,
146 // A source order with no line items was imported with one custom "Items not recorded in source"
147 // line at the order total (human-approved policy); each is also a Cutover Operator item.
148 'order-line-items-not-recorded' ,
149 ]);
150
151 // Mutually exclusive per-order buyer outcomes. When a report carries any of them, they
152 // must sum to the order population, like the payment partition.
153 const BUYER_OUTCOMES = Object. freeze ([
154 'buyer-member-linked' ,
155 'buyer-guest' ,
156 'buyer-contact-only' ,
157 'buyer-member-not-imported' ,
158 'buyer-member-email-mismatch' ,
159 'buyer-not-resolved' ,
160 ]);
161
162 // Actions the invoice preservation plan assigns per document. They are not outcomes, but they
163 // travel the same chain -- plan action becomes the writer's approval -- so registering them keeps
164 // producer and consumer on one vocabulary rather than two that happen to agree.
165 const PLAN_ACTIONS = Object. freeze ([
166 'preserve' ,
167 'offer-preservation' ,
168 'reference-only' ,
169 'refused' ,
170 ]);
171
172 // Causes attached to a partial-failure outcome (`errors.<stage>.cause`) by the order-history composite.
173 // Not outcomes and not counted, but handed back to callers, so they are registered here rather than
174 // living as stray literals the scan cannot account for.
175 const CAUSE_CODES = Object. freeze ([
176 'order-not-imported' ,
177 'add-payment-returned-no-id' ,
178 ]);
179
180 // What the report must carry a line for: every writer outcome that is not internal bookkeeping,
181 // plus every nested finding. Zero-valued lines are emitted, so absence of a line is a defect.
182 //
183 // The four category-attach-* codes are WRITER_OUTCOMES (so stageResiduals can validate them --
184 // see completion-report.js's STAGE_RESIDUAL_OUTCOMES), but they are deliberately listed here too:
185 // `REPORTABLE` feeds `rp-execution-policy/payments-report.js`'s ADDITIONAL_COUNTS, which is the
186 // PAYMENTS plan report -- a product-category outcome has no business appearing as a merchant-facing
187 // line on a document about payment providers and invoices. Product category attachment is reported
188 // through completion-report.js's stageResiduals mechanism directly (see
189 // collectCategoryAttachStageResiduals in stores-category-attach.js), which is the correct and only
190 // channel for it.
191 const INTERNAL_ONLY = Object. freeze ([ 'target-read' , 'create' ,
192 'category-attach-written' , 'category-attach-reconciled-existing' , 'category-attach-unverified' , 'category-attach-failed' ]);
193 const REPORTABLE = Object. freeze ([
194 ... WRITER_OUTCOMES . filter (( code ) => ! INTERNAL_ONLY . includes (code)),
195 ... FINDING_CODES ,
196 ]);
197
198 module . exports = {
199 PAYMENT_OUTCOMES,
200 BUYER_OUTCOMES,
201 PLAN_ACTIONS,
202 CAUSE_CODES,
203 WRITER_OUTCOMES,
204 FINDING_CODES,
205 INTERNAL_ONLY,
206 REPORTABLE,
207 };