Setting the file. One moment.
Stores Verification · Rp Target Wix · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 38.167
Position 167 of 174
Type JavaScript
Size 16 KB
Lines 337 lib/ stores-verification.js
JavaScript · 337 lines · 16 KB
(
'../../../lib/wix-auth.js'
);
8
9 const DEFAULT_VERIFIED_SUBSCRIPTION_PATHS = [
10 'product.subscriptionDetails' ,
11 'product.subscriptionDetails.allowOneTimePurchases' ,
12 'product.subscriptionDetails.subscriptions[]' ,
13 'product.subscriptionDetails.subscriptions[].id' ,
14 'product.subscriptionDetails.subscriptions[].title' ,
15 'product.subscriptionDetails.subscriptions[].description' ,
16 'product.subscriptionDetails.subscriptions[].frequency' ,
17 'product.subscriptionDetails.subscriptions[].interval' ,
18 'product.subscriptionDetails.subscriptions[].autoRenewal' ,
19 ];
20
21 function timestamp () {
22 return new Date (). toISOString ();
23 }
24
25 function defaultProbeProduct ({ marker , name } = {}) {
26 const suffix = marker || `rp-probe-${ Date . now () }` ;
27 return {
28 name: name || `RePlatform subscription probe ${ suffix }` ,
29 productType: 'PHYSICAL' ,
30 visible: false ,
31 sku: suffix,
32 price: { actualPrice: { amount: '1.00' } },
33 subscriptionDetails: {
34 allowOneTimePurchases: true ,
35 subscriptions: [{
36 title: 'Monthly delivery' ,
37 description: 'Ships every month' ,
38 frequency: 'MONTH' ,
39 interval: 1 ,
40 autoRenewal: true ,
41 }],
42 },
43 };
44 }
45
46 function artifactBase ({ command , siteId , endpoint , method }) {
47 return {
48 schemaVersion: 1 ,
49 command,
50 targetSiteIdentifier: siteId || null ,
51 endpoint,
52 method,
53 status: 'unknown' ,
54 verifiedPaths: [],
55 constraintsDiscovered: [],
56 probeRecordId: null ,
57 cleanup: { attempted: false , status: 'not_applicable' },
58 warnings: [],
59 recoveryInstructions: [],
60 timestamp: timestamp (),
61 };
62 }
63
64 function valueAtPath ( root , pathExpr ) {
65 const parts = String (pathExpr). split ( '.' );
66 let values = [root];
67 for ( const part of parts) {
68 const arrayPart = part. endsWith ( '[]' ) ? part. slice ( 0 , - 2 ) : null ;
69 const key = arrayPart || part;
70 const next = [];
71 for ( const value of values) {
72 if ( ! value || typeof value !== 'object' ) continue ;
73 const child = value[key];
74 if (arrayPart) {
75 if (Array. isArray (child)) next. push ( ... child);
76 } else {
77 next. push (child);
78 }
79 }
80 values = next;
81 }
82 return values. filter (( value ) => value !== undefined && value !== null );
83 }
84
85 function verifyPaths ( root , paths ) {
86 return paths. map (( pathExpr ) => ({
87 path: pathExpr,
88 present: valueAtPath (root, pathExpr). length > 0 ,
89 }));
90 }
91
92 function queryFilterByMarker ({ markerPath , markerValue }) {
93 if ( ! markerPath || markerValue == null ) {
94 throw new Error ( 'product-by-source-marker requires --marker-path and --marker-value' );
95 }
96 return {
97 filter: { [markerPath]: { $eq: markerValue } },
98 paging: { limit: 100 , offset: 0 },
99 };
100 }
101
102 async function writeArtifact ( file , result ) {
103 if ( ! file) return result;
104 fs. mkdirSync (path. dirname (file), { recursive: true });
105 fs. writeFileSync (file, `${ JSON . stringify ( result , null , 2 ) } \n ` );
106 return result;
107 }
108
109 async function verifyStoresSubscriptionCreate ({ wix , siteId , artifactPath , probeProduct , marker , cleanup = true } = {}) {
110 const createRequest = w. buildCreateStoresProductRequest (probeProduct || defaultProbeProduct ({ marker }));
111 const artifact = artifactBase ({
112 command: 'stores subscription-create' ,
113 siteId,
114 endpoint: createRequest.url,
115 method: createRequest.method,
116 });
117
118 let createdProduct;
119 try {
120 const expected = verification. subscriptionExpected (createRequest.body.product);
121 createdProduct = ( await wix. send (createRequest)).product;
122 artifact.probeRecordId = createdProduct && createdProduct.id;
123 if ( ! require ( './bulk-results' ). opaqueId (artifact.probeRecordId) || createdProduct._dryRunPlaceholder) throw new Error ( 'create response did not include a live product.id' );
124 artifact.valueEvidence = { expected, targetId: artifact.probeRecordId, actual: null };
125 await writeArtifact (artifactPath, artifact);
126
127 const getRequest = w. buildGetStoresProductRequest (artifact.probeRecordId);
128 artifact.readback = { endpoint: getRequest.url, method: getRequest.method };
129 const readProduct = ( await wix. send (getRequest)).product;
130 artifact.verifiedPaths = verifyPaths ({ product: readProduct }, DEFAULT_VERIFIED_SUBSCRIPTION_PATHS );
131 artifact.constraintsDiscovered = w. STORES_SUBSCRIPTION_CONTRACT .constraints. map (( constraint ) => ({ ... constraint }));
132 artifact.valueEvidence.actual = readProduct;
133 artifact.comparison = verification. verifySubscriptionEvidence (artifact.valueEvidence);
134 artifact.status = artifact.comparison.verified ? 'passed' : 'failed' ;
135 if (artifact.status === 'failed' ) {
136 artifact.warnings. push ( 'Subscription readback identity or values do not match the submitted product.' );
137 }
138 } catch (error) {
139 if (wixAuth. isRunInterruption (error)) throw error;
140 artifact.status = 'failed' ;
141 artifact.error = error && error.message ? error.message : String (error);
142 } finally {
143 if (cleanup && artifact.probeRecordId) {
144 artifact.cleanup.attempted = true ;
145 artifact.cleanup.endpoint = w. buildDeleteStoresProductRequest (artifact.probeRecordId).url;
146 artifact.cleanup.method = 'DELETE' ;
147 try {
148 await wix. send (w. buildDeleteStoresProductRequest (artifact.probeRecordId));
149 artifact.cleanup.status = 'deleted' ;
150 } catch (error) {
151 if (wixAuth. isRunInterruption (error)) throw error;
152 artifact.cleanup.status = 'failed' ;
153 artifact.cleanup.error = error && error.message ? error.message : String (error);
154 artifact.warnings. push ( `Probe product cleanup failed for ${ artifact . probeRecordId }.` );
155 artifact.recoveryInstructions. push (
156 `Delete probe product ${ artifact . probeRecordId } from Wix Stores or run: verify stores delete-probe --product-id ${ artifact . probeRecordId }` ,
157 );
158 }
159 }
160 }
161
162 return writeArtifact (artifactPath, artifact);
163 }
164
165 // With a catalog baseline the count is split: `baseline` is products that were on the
166 // store before this import and are still there, `runOwned` the rest. That needs every product id,
167 // so the whole catalog is read; without a baseline the single-page totalCount read is kept.
168 async function verifyStoresProductCount ({ wix , siteId , artifactPath , query = { paging: { limit: 1 , offset: 0 } }, baselineIds = null } = {}) {
169 const request = w. buildQueryStoresProductsRequest (query);
170 const artifact = artifactBase ({
171 command: 'stores product-count' ,
172 siteId,
173 endpoint: request.url,
174 method: request.method,
175 });
176 try {
177 if (baselineIds) {
178 const products = await w. queryAllStoresProducts (wix);
179 const baseline = products. filter (( product ) => baselineIds. has (product.id)). length ;
180 Object. assign (artifact, { count: products. length , total: products. length , baseline, runOwned: products. length - baseline });
181 artifact.status = 'passed' ;
182 return writeArtifact (artifactPath, artifact);
183 }
184 const response = await wix. send (request);
185 const products = response.products || [];
186 artifact.count = Number. isInteger (response.totalCount) ? response.totalCount : products. length ;
187 artifact.status = 'passed' ;
188 } catch (error) {
189 if (wixAuth. isRunInterruption (error)) throw error;
190 artifact.status = 'failed' ;
191 artifact.error = error && error.message ? error.message : String (error);
192 }
193 return writeArtifact (artifactPath, artifact);
194 }
195
196 async function verifyStoresProductBySourceMarker ({ wix , siteId , artifactPath , markerPath , markerValue } = {}) {
197 const query = queryFilterByMarker ({ markerPath, markerValue });
198 const request = w. buildQueryStoresProductsRequest (query);
199 const artifact = artifactBase ({
200 command: 'stores product-by-source-marker' ,
201 siteId,
202 endpoint: request.url,
203 method: request.method,
204 });
205 artifact.marker = { path: markerPath, value: markerValue };
206 try {
207 const response = await wix. send (request);
208 artifact.products = (response.products || []). map (( product ) => ({
209 id: product.id,
210 name: product.name,
211 slug: product.slug,
212 revision: product.revision,
213 }));
214 artifact.count = artifact.products. length ;
215 artifact.status = 'passed' ;
216 } catch (error) {
217 if (wixAuth. isRunInterruption (error)) throw error;
218 artifact.status = 'failed' ;
219 artifact.error = error && error.message ? error.message : String (error);
220 }
221 return writeArtifact (artifactPath, artifact);
222 }
223
224 async function verifyStoresDeleteProbe ({ wix , siteId , artifactPath , productId } = {}) {
225 if ( ! productId) throw new Error ( 'delete-probe requires --product-id' );
226 const request = w. buildDeleteStoresProductRequest (productId);
227 const artifact = artifactBase ({
228 command: 'stores delete-probe' ,
229 siteId,
230 endpoint: request.url,
231 method: request.method,
232 });
233 artifact.probeRecordId = productId;
234 artifact.cleanup = { attempted: true , endpoint: request.url, method: request.method, status: 'unknown' };
235 try {
236 await wix. send (request);
237 artifact.cleanup.status = 'deleted' ;
238 artifact.status = 'passed' ;
239 } catch (error) {
240 if (wixAuth. isRunInterruption (error)) throw error;
241 artifact.cleanup.status = 'failed' ;
242 artifact.status = 'failed' ;
243 artifact.error = error && error.message ? error.message : String (error);
244 artifact.warnings. push ( `Probe product cleanup failed for ${ productId }.` );
245 artifact.recoveryInstructions. push ( `Delete probe product ${ productId } manually in Wix Stores and keep this artifact with the cleanup evidence.` );
246 }
247 return writeArtifact (artifactPath, artifact);
248 }
249
250 // Runs only against an explicitly selected, already-muted test target. All products are
251 // synthetic and hidden. The artifact is saved as IDs are acquired so cleanup is recoverable.
252 async function verifyStoresInventoryImport ({ wix , siteId , artifactPath } = {}) {
253 const inventory = require ( './stores-inventory.js' );
254 const build = require ( './wix-build.js' );
255 const artifact = artifactBase ({ command: 'stores inventory-import' , siteId, endpoint: '/stores/v3/bulk/inventory-items/create' , method: 'POST' });
256 artifact.probeProductIds = [];
257 artifact.checks = [];
258 let stockWrites = 0 ;
259 const client = wix;
260 wix = { ... client, send ( request ) {
261 if (request.method !== 'GET' && / \/ inventory-items(?: \/ |$ )/ . test (request.url) && ! request.url. endsWith ( '/query' )) stockWrites ++ ;
262 return client. send (request);
263 } };
264 const check = ( name , passed , detail ) => {
265 artifact.checks. push ({ name, passed, detail });
266 writeArtifact (artifactPath, artifact);
267 if ( ! passed) throw new Error ( `Inventory probe failed: ${ name }` );
268 };
269 const marker = `inventory-probe-${ Date . now () }` ;
270 const payload = ( index , stock ) => build. buildProduct ({ name: `${ marker }-${ index }` , visible: false , productType: 'PHYSICAL' , variants: [{ price: 1 , ... (stock || {}) }] });
271 try {
272 const mute = await w. getSiteMuteState (wix);
273 if ( ! mute || mute.muted !== true ) throw new Error ( 'Inventory probe requires an already-muted disposable test target' );
274 const locationId = await inventory. resolveInventoryLocation (wix);
275 const rows = [];
276 for ( const [ index , state ] of [{ inStock: true }, { inStock: false }, { quantity: 0 }, { quantity: 7 }]. entries ()) {
277 const product = await w. createStoresProduct (wix, payload (index));
278 if ( ! product || ! product.id) throw new Error ( 'Probe product create returned no identity' );
279 artifact.probeProductIds. push (product.id); writeArtifact (artifactPath, artifact);
280 const variants = product.variantsInfo && product.variantsInfo.variants;
281 if ( ! Array. isArray (variants) || variants. length !== 1 || ! variants[ 0 ].id) throw new Error ( 'Probe product did not return one variant identity' );
282 rows. push ({ productId: product.id, variantId: variants[ 0 ].id, inventory: state });
283 }
284 const created = await inventory. reconcileInventory (wix, rows, { locationId, expectedCount: 4 , verifyAttempts: 5 });
285 check ( 'mixed stock create and read-back' , created.complete && created.counts.created === 4 , created);
286 const changed = rows. map (( r , i ) => ({ ... r, inventory: i % 2 ? { inStock: false } : { quantity: 3 } }));
287 const updated = await inventory. reconcileInventory (wix, changed, { locationId, expectedCount: 4 , verifyAttempts: 5 });
288 check ( 'revisioned updates and tracking-mode changes' , updated.complete && updated.counts.updated === 3 , updated);
289 const beforeRerun = stockWrites;
290 const rerun = await inventory. reconcileInventory (wix, changed, { locationId, expectedCount: 4 });
291 check ( 'matching rerun performs no stock writes' , rerun.complete && rerun.counts.reconciled === 4 && stockWrites === beforeRerun, { ... rerun, observedStockWrites: stockWrites - beforeRerun });
292 const singleProduct = await w. createStoresProduct (wix, payload ( 'single' ));
293 if ( ! singleProduct || ! singleProduct.id) throw new Error ( 'Single probe missing product identity' );
294 artifact.probeProductIds. push (singleProduct.id); writeArtifact (artifactPath, artifact);
295 const singleRow = { productId: singleProduct.id, variantId: singleProduct.variantsInfo.variants[ 0 ].id, inventory: { inStock: false } };
296 await w. createInventoryItem (wix, { productId: singleRow.productId, variantId: singleRow.variantId, inStock: false , trackQuantity: false });
297 const single = await inventory. reconcileInventory (wix, [singleRow], { locationId, expectedCount: 1 });
298 check ( 'single-create helper and read-back' , single.complete && single.counts.reconciled === 1 , single);
299 const bulk = await w. bulkCreateStoresProductsWithInventory (wix, [ payload ( 'inline-false' , { inStock: false }), payload ( 'inline-quantity' , { inventoryTracked: true , inventoryQuantity: 5 })], { returnEntity: true });
300 for ( const result of bulk.results) if (result.success && result.productId) artifact.probeProductIds. push (result.productId);
301 writeArtifact (artifactPath, artifact);
302 check ( 'inline bulk product creation' , bulk.succeeded. length === 2 && bulk.unaccounted === 0 , { successes: bulk.succeeded. length , unaccounted: bulk.unaccounted });
303 const inlineRows = bulk.succeeded. map (( r ) => {
304 const variants = r.product && r.product.variantsInfo && r.product.variantsInfo.variants;
305 if ( ! variants || variants. length !== 1 || ! variants[ 0 ].id) throw new Error ( 'Inline probe missing variant identity' );
306 return { productId: r.productId, variantId: variants[ 0 ].id, inventory: r.inputProduct.variantsInfo.variants[ 0 ].inventoryItem };
307 });
308 const inline = await inventory. reconcileInventory (wix, inlineRows, { locationId, expectedCount: 2 });
309 check ( 'inline stock persisted without repair' , inline.complete && inline.counts.reconciled === 2 , inline);
310 artifact.status = 'passed' ;
311 } catch (error) { if (wixAuth. isRunInterruption (error)) throw error; artifact.status = 'failed' ; artifact.error = error.message; }
312 finally {
313 artifact.cleanup = { attempted: artifact.probeProductIds. length > 0 , status: artifact.probeProductIds. length ? 'deleted' : 'not_applicable' , failedProductIds: [] };
314 for ( const id of artifact.probeProductIds) {
315 try { await w. deleteStoresProduct (wix, id); }
316 catch (error) { if (wixAuth. isRunInterruption (error)) throw error; artifact.cleanup.failedProductIds. push (id); }
317 }
318 if (artifact.cleanup.failedProductIds. length ) {
319 artifact.cleanup.status = 'failed' ; artifact.status = 'failed' ;
320 artifact.recoveryInstructions. push ( 'Delete only the probe product IDs listed in cleanup.failedProductIds.' );
321 }
322 }
323 return writeArtifact (artifactPath, artifact);
324 }
325
326 module . exports = {
327 DEFAULT_VERIFIED_SUBSCRIPTION_PATHS,
328 defaultProbeProduct,
329 queryFilterByMarker,
330 valueAtPath,
331 verifyPaths,
332 verifyStoresSubscriptionCreate,
333 verifyStoresProductCount,
334 verifyStoresProductBySourceMarker,
335 verifyStoresDeleteProbe,
336 verifyStoresInventoryImport,
337 };