Setting the file. One moment.
Gateway Source Keys · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page 39.41
PLUGIN READ PLANS
This file
Number 39.15
Position 15 of 92
Type JavaScript
Size 12 KB
Lines 252 lib/ gateway-source-keys.js
JavaScript · 252 lines · 12 KB
14
// 2. Key presence is not attribution either. Many orders carry two families' keys at once, and
15 // an offline (cash-on-delivery) order is observed carrying a gateway-shaped key. So "first
16 // populated key wins" invents a precedence the data does not support.
17 //
18 // What survives both is: verified field semantics AND a registry-recognized relationship between
19 // the field's producer and the order's method, where that relationship may be direct or a
20 // separately verified layered integration. Everything else is reported, never written. A reported
21 // candidate is never truncated to fit, and never becomes evidence that a payment exists -- that
22 // gate belongs to the payment-request mapping and needs a paid date.
23
24 const fs = require ( 'node:fs' );
25 const path = require ( 'node:path' );
26
27 const REGISTRY_PATH = path. join (__dirname, 'gateway-source-keys.json' );
28
29 function loadRegistry ( registryPath = REGISTRY_PATH ) {
30 return JSON . parse (fs. readFileSync (registryPath, 'utf8' ));
31 }
32
33 // A meta value counts as populated only when it survives trimming. WooCommerce stores empty
34 // strings for keys a gateway touched but never filled, and an empty string is not a reference.
35 function populated ( value ) {
36 if (value === null || value === undefined ) return null ;
37 const text = String (value). trim ();
38 return text. length > 0 ? text : null ;
39 }
40
41 function matchesSlug ( slug , direct ) {
42 if ( ! slug) return false ;
43 const slugs = (direct && direct.slugs) || [];
44 const prefixes = (direct && direct.slugPrefixes) || [];
45 return slugs. includes (slug) || prefixes. some (( prefix ) => slug. startsWith (prefix));
46 }
47
48 // Recognition is the whole safeguard, so it is deliberately narrow: a direct method match, or a
49 // layered integration whose `verified` flag is literally true. An unverified layered entry is
50 // present in the registry so the relationship can be REPRESENTED without being trusted.
51 function recognizesMethod ( family , paymentMethod ) {
52 if ( ! paymentMethod) return false ;
53 if ( matchesSlug (paymentMethod, family.methodRelationships && family.methodRelationships.direct)) {
54 return true ;
55 }
56 const layered = (family.methodRelationships && family.methodRelationships.layered) || [];
57 return layered. some (
58 ( entry ) => entry.verified === true && (entry.frontEndMethodSlugs || []). includes (paymentMethod),
59 );
60 }
61
62 function classifyMethod ( paymentMethod , registry = loadRegistry ()) {
63 const rules = registry.methodClassification;
64 if ( ! paymentMethod) return 'unknown' ;
65 if (rules.offlineSlugs. includes (paymentMethod)) return 'offline' ;
66 if (rules.onlineSlugs. includes (paymentMethod)) return 'online' ;
67 if (rules.onlineSlugPrefixes. some (( prefix ) => paymentMethod. startsWith (prefix))) return 'online' ;
68 return 'unknown' ;
69 }
70
71 // The payment-request mapping turns this into: true for known offline, false for known online, and OMITTED
72 // when unknown. Omission is not a synonym for false -- guessing "offline" for an unrecognised
73 // slug is how a card payment ends up labelled as cash.
74 function resolveOfflinePayment ( paymentMethod , registry = loadRegistry ()) {
75 const classification = classifyMethod (paymentMethod, registry);
76 if (classification === 'offline' ) return { offlinePayment: true , findings: [] };
77 if (classification === 'online' ) return { offlinePayment: false , findings: [] };
78 return {
79 offlinePayment: undefined ,
80 findings: [{ code: 'unclassified-method' , paymentMethod: paymentMethod || null }],
81 };
82 }
83
84 function referenceLimit ( registry ) {
85 return registry.targetLimits.providerTransactionId;
86 }
87
88 // Why every populated family candidate on an order was or was not eligible, independent of
89 // whether one of them won.
90 function familyCandidateFindings ( meta , paymentMethod , registry , limit ) {
91 const findings = [];
92 for ( const family of registry.families) {
93 const recognized = recognizesMethod (family, paymentMethod);
94 for ( const field of family.referenceFields) {
95 const value = populated (meta[field]);
96 if ( ! value) continue ;
97 if (family.evidence !== 'verified' ) findings. push ({ code: 'unverified-gateway-key' , family: family.family, field, paymentMethod });
98 if ( ! recognized) findings. push ({ code: 'incompatible-gateway-metadata' , family: family.family, field, paymentMethod });
99 if (value. length > limit) findings. push ({ code: 'reference-over-length' , family: family.family, field, length: value. length , limit });
100 }
101 }
102 return findings;
103 }
104
105 function resolveTransactionReference ({ meta = {}, paymentMethod = null , registry = loadRegistry () } = {}) {
106 const findings = [];
107 const limit = referenceLimit (registry);
108
109 // Step 1: the canonical WooCommerce reference, valid for any method. An over-length canonical
110 // value does not short-circuit the search -- it is reported and the family candidates still run.
111 for ( const field of registry.canonicalReference.referenceFields) {
112 const value = populated (meta[field]);
113 if ( ! value) continue ;
114 if (value. length > limit) {
115 findings. push ({ code: 'reference-over-length' , family: registry.canonicalReference.family, field, length: value. length , limit });
116 continue ;
117 }
118 // Report the other populated candidates too. Returning bare here hid every unverified or
119 // incompatible gateway key on the order, so a store's verification backlog looked smaller
120 // than it is purely because the canonical field happened to be filled in.
121 return {
122 reference: value,
123 source: { family: registry.canonicalReference.family, field },
124 findings: [ ... findings, ... familyCandidateFindings (meta, paymentMethod, registry, limit)],
125 };
126 }
127
128 // Step 2: gateway-specific candidates. Every disqualifying reason is reported, not just the
129 // first one, because "unverified semantics" and "unrecognized method relationship" are separate
130 // pieces of work and a run that hides one of them understates what verification would buy.
131 const eligible = [];
132 for ( const family of registry.families) {
133 const recognized = recognizesMethod (family, paymentMethod);
134 if (family.evidence === 'unmapped' && recognized) {
135 findings. push ({ code: 'unmapped-gateway-family' , family: family.family, paymentMethod });
136 }
137 for ( const field of family.referenceFields) {
138 const value = populated (meta[field]);
139 if ( ! value) continue ;
140 const reasons = [];
141 if (family.evidence !== 'verified' ) reasons. push ( 'unverified-gateway-key' );
142 if ( ! recognized) reasons. push ( 'incompatible-gateway-metadata' );
143 if (value. length > limit) reasons. push ( 'reference-over-length' );
144 if (reasons. length > 0 ) {
145 for ( const code of reasons) {
146 findings. push ({ code, family: family.family, field, paymentMethod });
147 }
148 continue ;
149 }
150 eligible. push ({ family: family.family, field, value });
151 }
152 }
153
154 const distinct = [ ...new Set (eligible. map (( candidate ) => candidate.value))];
155 if (distinct. length === 1 ) {
156 const winner = eligible. find (( candidate ) => candidate.value === distinct[ 0 ]);
157 return { reference: winner.value, source: { family: winner.family, field: winner.field }, findings };
158 }
159 if (distinct. length > 1 ) {
160 findings. push ({
161 code: 'ambiguous-transaction-reference' ,
162 families: [ ...new Set (eligible. map (( candidate ) => candidate.family))],
163 distinctValues: distinct. length ,
164 });
165 return { reference: null , source: null , findings };
166 }
167
168 findings. push ({ code: 'no-transaction-reference' , paymentMethod });
169 return { reference: null , source: null , findings };
170 }
171
172 // Card brand and last four ride the same recognition rule as the reference. They are display
173 // details only: nothing here implies Wix holds a chargeable credential.
174 function resolveCardDisplay ({ meta = {}, paymentMethod = null , registry = loadRegistry () } = {}) {
175 const findings = [];
176 const limits = registry.targetLimits;
177 for ( const family of registry.families) {
178 const fields = family.cardDisplayFields || {};
179 const lastFourField = fields.lastFourDigits;
180 const brandField = fields.brand;
181 if ( ! lastFourField && ! brandField) continue ;
182
183 const lastFour = lastFourField ? populated (meta[lastFourField]) : null ;
184 const brand = brandField ? populated (meta[brandField]) : null ;
185 if ( ! lastFour && ! brand) continue ;
186
187 const recognized = recognizesMethod (family, paymentMethod);
188 if (family.evidence !== 'verified' || ! recognized) {
189 findings. push ({ code: 'incompatible-gateway-metadata' , family: family.family, fields: [lastFourField, brandField]. filter (Boolean), paymentMethod });
190 continue ;
191 }
192
193 const cardDisplay = {};
194 if (lastFour) {
195 if (lastFour. length <= limits.lastFourDigits) cardDisplay.lastFourDigits = lastFour;
196 // Dropped silently, an over-length card field is indistinguishable from an absent one.
197 else findings. push ({ code: 'card-display-over-length' , family: family.family, field: lastFourField, length: lastFour. length , limit: limits.lastFourDigits });
198 }
199 if (brand) {
200 if (brand. length <= limits.cardBrand) cardDisplay.brand = brand;
201 else findings. push ({ code: 'card-display-over-length' , family: family.family, field: brandField, length: brand. length , limit: limits.cardBrand });
202 }
203 if (Object. keys (cardDisplay). length > 0 ) return { cardDisplay, family: family.family, findings };
204 }
205 return { cardDisplay: null , family: null , findings };
206 }
207
208 // Candidate coverage and defensible coverage are separate report lines by decision: a run that
209 // reports "94.8% of orders carry a reference" when it can only write three quarters of them has
210 // misdescribed its own fidelity. Callers surface BOTH.
211 function summarizeReferenceCoverage ( orders = [], registry = loadRegistry ()) {
212 const knownFields = new Set ([
213 ... registry.canonicalReference.referenceFields,
214 ... registry.families. flatMap (( family ) => family.referenceFields),
215 ]);
216 const summary = { orders: orders. length , candidates: 0 , defensible: 0 , findings: {} };
217 for ( const order of orders) {
218 const meta = order.meta || {};
219 if ([ ... knownFields]. some (( field ) => populated (meta[field]))) summary.candidates += 1 ;
220 const resolved = resolveTransactionReference ({ meta, paymentMethod: order.paymentMethod, registry });
221 if (resolved.reference) summary.defensible += 1 ;
222 for ( const finding of resolved.findings) {
223 summary.findings[finding.code] = (summary.findings[finding.code] || 0 ) + 1 ;
224 }
225 }
226 return summary;
227 }
228
229 // The read manifest for the payments import (read only what an approved outcome needs).
230 // Returned sorted and de-duplicated so a generated read plan is stable across runs -- an unstable
231 // key order changes the plan hash and makes integrity verification report false tampering.
232 function paymentReadKeys ( registry = loadRegistry ()) {
233 const keys = new Set (registry.paymentReadKeys.core);
234 for ( const field of registry.canonicalReference.referenceFields) keys. add (field);
235 for ( const family of registry.families) {
236 for ( const field of family.referenceFields) keys. add (field);
237 for ( const field of Object. values (family.cardDisplayFields || {})) keys. add (field);
238 }
239 return [ ... keys]. sort ();
240 }
241
242 module . exports = {
243 REGISTRY_PATH,
244 loadRegistry,
245 paymentReadKeys,
246 classifyMethod,
247 resolveOfflinePayment,
248 resolveTransactionReference,
249 resolveCardDisplay,
250 summarizeReferenceCoverage,
251 recognizesMethod,
252 };