Setting the file. One moment.
Read Back Comparison · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function walk
— line 101
This file
Number 45.131
Position 131 of 182
Type JavaScript
Size 10 KB
Lines 216 lib/ read-back-comparison.js
JavaScript · 216 lines · 10 KB
12 const canonical = p => String (p). replace ( / \[ \d + \] / g , '[]' );
13 const old = require ( './write-verification' );
14 const { storesProductMediaItemLanded , normalizeV5AddressItem , V5_ADDRESS_ITEM_KEYS } = require ( '../resources/rp-target-wix/lib/wix-writers' );
15 const empty = v => ! populated (v);
16 function decimal ( v ) {
17 if ( typeof v !== 'string' && typeof v !== 'number' ) return null ;
18 const m = / ^ ( [+-] ? )( \d + )(?: \. ( \d + )) ?$ / . exec ( String (v));
19 if ( ! m) return null ;
20 const whole = m[ 2 ]. replace ( / ^ 0 + (?= \d )/ , '' ),
21 fraction = (m[ 3 ] || '' ). replace ( /0 +$ / , '' );
22 return `${ m [ 1 ] === '-' && ( whole !== '0' || fraction ) ? '-' : ''}${ whole }${ fraction ? '.' + fraction : ''}` ;
23 }
24 function memberProjection ( value ) {
25 const m = value.member || value,
26 c = value.contact;
27 if ( ! c) return m;
28 return {
29 ... m,
30 contact: {
31 firstName: c.name?.first,
32 lastName: c.name?.last,
33 phones: [c.phone?.phone, ... (c.additionalPhones || []). map ( p => p.phone)]. filter ( v => v !== undefined ),
34 addresses: (c.addresses || []). map ( a => a.address),
35 // Members v1 writes contact.company and contact.jobTitle as strings; Contacts V5 reads them
36 // back as company { name, jobTitle }. Compared in the shape the member writer sent.
37 ... (c.company !== undefined ? v5Company (c.company) : {})
38 }
39 };
40 }
41 function v5Company ( company ) {
42 if ( ! company || typeof company !== 'object' || Array. isArray (company)) return { company };
43 return { company: company.name, ... (company.jobTitle !== undefined ? { jobTitle: company.jobTitle } : {}) };
44 }
45 // Contacts V5 stores an address item as { tag, address: {...} }; builders may emit it flat. Both
46 // sides are compared in one flat view: expected first takes the writer's own nesting, so a field
47 // is compared where the writer actually sent it.
48 function flatAddress ( item ) {
49 if ( ! item || typeof item !== 'object' || Array. isArray (item)) return item;
50 const { address , ... rest } = item;
51 return address && typeof address === 'object' && ! Array. isArray (address) ? { ... rest, ... address } : rest;
52 }
53 function contactProjection ( value , { sent = false } = {}) {
54 const c = value?.contact || value;
55 if ( ! c || ! Array. isArray (c.addresses)) return c;
56 return { ... c, addresses: c.addresses. map ( item => flatAddress (sent ? normalizeV5AddressItem (item) : item)) };
57 }
58 // Variant choices read back as ids, and the stock flag read back as inventoryStatus.inStock, are
59 // compared through the same view the create verifier uses (write-verification.js
60 // variantComparisonView).
61 function productProjection ( product ) {
62 const variants = product?.variantsInfo?.variants;
63 if ( ! Array. isArray (variants)) return product;
64 return {
65 ... product,
66 variantsInfo: {
67 ... product.variantsInfo,
68 variants: variants. map ( v => old. variantComparisonView (v, product))
69 }
70 };
71 }
72 // Kept equal to order-history.js PLACEHOLDER_LINE_NAME (asserted by order-placeholder-line-test.js).
73 const ORDER_PLACEHOLDER_LINE_NAME = 'Items not recorded in source' ;
74 function compareEntity ( entity , expected , rawActual ) {
75 const e = entity === 'contact' ? contactProjection (expected, { sent: true }) : expected[entity] || expected,
76 a = entity === 'member' ? memberProjection (rawActual) : entity === 'product' ? productProjection (rawActual[entity] || rawActual) : entity === 'contact' ? contactProjection (rawActual) : rawActual[entity] || rawActual,
77 checks = [];
78 const readPath = p => {
79 p = canonical (p);
80 if (entity === 'contact' ) {
81 const field = / ^ addresses \[\]\. ( [ ^ .[] + )( . * ) $ / . exec (p);
82 return field && ! V5_ADDRESS_ITEM_KEYS . has (field[ 1 ]) ? `addresses[].address.${ field [ 1 ] }${ field [ 2 ] }` : p;
83 }
84 if (entity !== 'member' ) return p;
85 if (p === 'contact.firstName' ) return 'contact.name.first' ;
86 if (p === 'contact.lastName' ) return 'contact.name.last' ;
87 if (p === 'contact.company' ) return 'contact.company.name' ;
88 if (p === 'contact.jobTitle' ) return 'contact.company.jobTitle' ;
89 if (p. startsWith ( 'contact.addresses[].' )) return p. replace ( 'contact.addresses[].' , 'contact.addresses[].address.' );
90 return p. startsWith ( 'contact.' ) ? p : `member.${ p }` ;
91 };
92 function add ( p , wanted , actual , matches ) {
93 checks. push ({
94 writePath: canonical ( `${ entity }.${ p }` ),
95 readPath: readPath (p),
96 matches,
97 kind: matches ? 'verified' : empty (actual) && ! empty (wanted) ? 'dropped' : 'changed'
98 });
99 }
100 const serverOwned = domainContract (entity).readBackProjection.serverOwned;
101 function walk ( wanted , actual , p ) {
102 const server = serverOwned. find ( row => row.path === `${ entity }.${ canonical ( p ) }` );
103 if (server) {
104 checks. push ({
105 writePath: server.path,
106 readPath: canonical (p),
107 matches: true ,
108 kind: 'unverifiable' ,
109 reason: server.reason
110 });
111 return ;
112 }
113 // A tracked variant quantity is not on the Catalog V3 product read (it lives in Inventory
114 // Items); verifyCatalogInventory (stores-inventory.js) reads and verifies it after the write.
115 if (entity === 'product' && / ^ variantsInfo \. variants \[ \d * \]\. inventoryItem \. quantity $ / . test ( canonical (p))) {
116 checks. push ({ writePath: canonical ( `${ entity }.${ p }` ), readPath: canonical (p), matches: true , kind: 'unverifiable' ,
117 reason: 'Catalog V3 product read carries no tracked quantity; the inventory reconcile verifies it (Inventory Items API)' });
118 return ;
119 }
120 // A source order with no line items lands with the one approved placeholder line (order-history
121 // withPlaceholderLineItem, a human-approved policy). Verified as exactly that line at the
122 // order total, never as a changed field; anything else still fails.
123 if (entity === 'order' && p === 'lineItems' && empty (wanted) && Array. isArray (actual) && actual. length ) {
124 const [ line ] = actual, total = e?.priceSummary?.total?.amount;
125 const ok = actual. length === 1 && line?.productName?.original === ORDER_PLACEHOLDER_LINE_NAME && line.quantity === 1
126 && decimal (line.price?.amount) !== null && decimal (line.price.amount) === decimal (total ?? '0' );
127 checks. push ({ writePath: 'order.lineItems' , readPath: 'lineItems' , matches: ok, kind: ok ? 'verified' : 'changed' ,
128 ... (ok ? { reason: 'approved placeholder line for a source order with no line items (order-line-items-not-recorded)' } : {}) });
129 return ;
130 }
131 if ( empty (wanted)) {
132 // Stores V3 derives shippingWeightRange beside the required empty physicalProperties.
133 const serverDerived = entity === 'product' && p === 'physicalProperties' && old. onlyServerPhysical (actual);
134 add (p, wanted, actual, empty (actual) || serverDerived);
135 return ;
136 }
137 if (Array. isArray (wanted) && / ^ media \. itemsInfo \. items $ / . test (p) && entity === 'product' ) {
138 const result = old. compareProjection (wanted, actual, {
139 path: p
140 });
141 const landed = Array. isArray (actual) && actual. every (storesProductMediaItemLanded);
142 add (p, wanted, actual, result.verified && landed);
143 return ;
144 }
145 if (p === 'plainDescription' && entity === 'product' ) {
146 add (p, wanted, actual, old. compareProjection (wanted, actual, {
147 path: p
148 }).verified);
149 return ;
150 }
151 if (Array. isArray (wanted)) {
152 if ( ! Array. isArray (actual)) {
153 for ( const item of wanted) walk (item, undefined , `${ p }[]` );
154 return ;
155 }
156 if (entity === 'member' && p === 'contact.phones' ) {
157 const left = [ ...new Set (wanted)]. sort (),
158 right = [ ...new Set (actual)]. sort ();
159 add (p, wanted, actual, isDeepStrictEqual (left, right));
160 return ;
161 }
162 if (entity === 'contact' && p === 'addresses' ) {
163 // One-to-one, order-free pairing shared with verifyContact.
164 if (wanted. length !== actual. length ) add ( `${ p }[]` , wanted, actual, false );
165 // A candidate pair matches only if this walk itself verifies it; its trial checks are discarded.
166 const verifies = ( value , candidate ) => {
167 const mark = checks. length ;
168 walk (value, candidate, `${ p }[]` );
169 const ok = checks. slice (mark). every ( check => check.matches);
170 checks. length = mark;
171 return ok;
172 };
173 for ( const [ value , match ] of old. pairUnordered (wanted, actual, verifies)) walk (value, match, `${ p }[]` );
174 return ;
175 }
176 if (entity === 'member' && p === 'contact.addresses' || entity === 'order' && p === 'appliedDiscounts' ) {
177 const subset = ( w , a ) => w && typeof w === 'object' ? Array. isArray (w) ? Array. isArray (a) && w. length === a. length && w. every (( v , i ) => subset (v, a[i])) : Object. entries (w). every (([ k , v ]) => subset (v, a?.[k])) : isDeepStrictEqual (w, a);
178 const remaining = [ ... actual];
179 if (wanted. length !== actual. length ) add ( `${ p }[]` , wanted, actual, false );
180 for ( const value of wanted) {
181 const at = remaining. findIndex ( candidate => subset (value, candidate));
182 walk (value, at < 0 ? undefined : remaining. splice (at, 1 )[ 0 ], `${ p }[]` );
183 }
184 return ;
185 }
186 if (entity === 'order' && p === 'lineItems' ) {
187 for ( const item of wanted) {
188 const matches = actual. filter ( x => x.id === item.id);
189 if ( ! item.id || matches. length !== 1 ) add ( `${ p }[]` , item, null , false ); else walk (item, matches[ 0 ], `${ p }[]` );
190 }
191 return ;
192 }
193 add (p, wanted, actual, wanted. length === actual. length );
194 wanted. forEach (( v , i ) => walk (v, actual[i], `${ p }[${ i }]` ));
195 return ;
196 }
197 if (wanted && typeof wanted === 'object' ) {
198 for ( const [ k , v ] of Object. entries (wanted)) walk (v, actual?.[k], p ? `${ p }.${ k }` : k);
199 return ;
200 }
201 let equal = isDeepStrictEqual (wanted, actual);
202 if ( /( ^| \. )(loginEmail | email) $ / . test (p)) equal = old. email (wanted) === old. email (actual); else if ( /(Date | At) $ / . test (p) && typeof wanted === 'string' && Number. isFinite (Date. parse (wanted))) equal = Number. isFinite (Date. parse (actual)) && Date. parse (wanted) === Date. parse (actual); else if ( /( ^| \. )(amount | price | priceBeforeDiscount | priceAfterDiscount | total | subtotal | taxAmount | discountAmount) $ / . test (p) && decimal (wanted) !== null ) equal = decimal (wanted) === decimal (actual);
203 add (p, wanted, actual, equal);
204 }
205 walk (e, a, '' );
206 return {
207 verified: checks. length > 0 && checks. every ( c => c.matches),
208 checks
209 };
210 }
211 module . exports = {
212 compareEntity,
213 memberProjection,
214 contactProjection,
215 decimal
216 };