Setting the file. One moment.
Bundle Canonical · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page 39.41
PLUGIN READ PLANS
lib/ bundle-canonical.js
JavaScript · 166 lines · 8 KB
14
// The bundle's price is never computed here: the record's own `price` is the product's price.
15 // The product type decides: meta left on a product whose type is no longer `woosb` / `composite`
16 // (it was changed, or the plugin is off and WooCommerce reports it as a plain product) is not a
17 // bundle, because the source sells it as a plain product. It is reported as `staleBundleMeta`.
18
19 const BUNDLE_META = Object. freeze ([ 'woosb_ids' ]);
20 const COMPOSITE_META = Object. freeze ([ 'wooco_components' ]);
21 // Every meta key the field contract carries to cutover for these plugins (A.2 of the bundle rule):
22 // the composition plus the pricing settings that only describe how the source priced it.
23 const CARRIED_META_KEYS = Object. freeze ([
24 'woosb_ids' , 'woosb_disable_auto_price' , 'woosb_discount' , 'woosb_discount_amount' , 'woosb_optional_products' ,
25 'wooco_components' , 'wooco_pricing' , 'wooco_discount_percent' , 'wooco_custom_price' ,
26 ]);
27
28 function metaMap ( record ) {
29 const out = new Map ();
30 for ( const row of Array. isArray (record?.meta_data) ? record.meta_data : []) {
31 if (row && typeof row.key === 'string' ) out. set (row.key, row.value);
32 }
33 return out;
34 }
35
36 const present = ( value ) => value !== undefined && value !== null && value !== '' && ! (Array. isArray (value) && value. length === 0 )
37 && ! ( typeof value === 'object' && ! Array. isArray (value) && Object. keys (value). length === 0 );
38
39 function positiveInt ( value , fallback ) {
40 const n = typeof value === 'number' ? value : Number. parseFloat ( String (value ?? '' ). trim ());
41 if ( ! Number. isFinite (n) || n <= 0 ) return fallback;
42 return Math. max ( 1 , Math. round (n));
43 }
44
45 function idOf ( value ) {
46 const text = String (value ?? '' ). trim ();
47 return / ^ \d +$ / . test (text) && text !== '0' ? text : null ;
48 }
49
50 const truthy = ( value ) => value === true || [ 'on' , 'yes' , '1' , 'true' ]. includes ( String (value ?? '' ). trim (). toLowerCase ());
51
52 // woosb_ids: current shape {<key>: {id, sku, qty}} (or an array of those); legacy 'id1/qty1,id2/qty2'.
53 function woosbComponents ( raw ) {
54 let items;
55 if ( typeof raw === 'string' ) {
56 const text = raw. trim ();
57 if (text. startsWith ( '{' ) || text. startsWith ( '[' )) {
58 try { return woosbComponents ( JSON . parse (text)); } catch { throw new Error ( 'woosb_ids is not valid JSON' ); }
59 }
60 items = text. split ( ',' ). map (( part ) => part. trim ()). filter (Boolean). map (( part ) => {
61 const [ id , qty ] = part. split ( '/' );
62 return { id, qty };
63 });
64 } else if (Array. isArray (raw)) items = raw;
65 else if (raw && typeof raw === 'object' ) items = Object. values (raw);
66 else throw new Error ( 'woosb_ids has an unknown shape' );
67 const components = items. map (( item , index ) => {
68 const id = idOf (item && item.id);
69 if ( ! id) throw new Error ( `woosb_ids item ${ index } has no product id` );
70 return { sourceProductId: id, sourceVariantId: null , quantity: positiveInt (item.qty, 1 ), optional: truthy (item.optional) };
71 });
72 if ( ! components. length ) throw new Error ( 'woosb_ids lists no products' );
73 return components;
74 }
75
76 function listOf ( value ) {
77 if (Array. isArray (value)) return value;
78 if ( typeof value === 'string' ) return value. split ( ',' ). map (( part ) => part. trim ()). filter (Boolean);
79 return [];
80 }
81
82 // wooco_components: {<key>: {name, type, products, categories, tags, exclude, optional, min, max, ...}}.
83 // `type` names where the group's choices come from: products (an explicit list) or a category/tag
84 // rule. min/max are the group's quantity limits (empty max = no limit).
85 function woocoGroups ( raw , { productsForRule } = {}) {
86 let value = raw;
87 if ( typeof value === 'string' ) {
88 try { value = JSON . parse (value); } catch { throw new Error ( 'wooco_components is not valid JSON' ); }
89 }
90 const items = Array. isArray (value) ? value : value && typeof value === 'object' ? Object. values (value) : null ;
91 if ( ! items || ! items. length ) throw new Error ( 'wooco_components lists no components' );
92 return items. map (( item , index ) => {
93 if ( ! item || typeof item !== 'object' ) throw new Error ( `wooco_components item ${ index } is not an object` );
94 const type = String (item.type || 'products' );
95 const optional = truthy (item.optional);
96 const min = optional ? 0 : positiveInt (item.min, 1 );
97 const maxRaw = String (item.max ?? '' ). trim ();
98 const max = maxRaw === '' ? null : Math. max (min || 1 , positiveInt (maxRaw, 1 ));
99 const exclude = listOf (item.exclude). map (idOf). filter (Boolean);
100 let candidates = [];
101 let candidateRule = null ;
102 if (type === 'products' ) {
103 candidates = listOf (item.products). map (idOf). filter (Boolean). map (( id ) => ({ sourceProductId: id, sourceVariantId: null }));
104 if ( ! candidates. length ) throw new Error ( `wooco_components item ${ index } lists no products` );
105 } else {
106 const terms = listOf (item[type] ?? item.categories ?? item.tags). map (idOf). filter (Boolean);
107 candidateRule = { source: type, termIds: terms, excludeProductIds: exclude };
108 if ( typeof productsForRule === 'function' ) {
109 candidates = [ ...new Set (( productsForRule (candidateRule) || []). map (idOf). filter (Boolean))]
110 . filter (( id ) => ! exclude. includes (id)). sort (). map (( id ) => ({ sourceProductId: id, sourceVariantId: null }));
111 }
112 }
113 return { label: typeof item.name === 'string' ? item.name : '' , min, max, optional, candidates, candidateRule };
114 });
115 }
116
117 function stockRuleOf ( record ) {
118 return record && record.manage_stock === true ? 'own' : 'unknown' ;
119 }
120
121 // productsForRule(rule) may resolve a category/tag group to product ids from the extracted catalog.
122 function canonicalBundleFromWooProduct ( record , { productsForRule } = {}) {
123 const meta = metaMap (record);
124 let woosb = BUNDLE_META . some (( key ) => present (meta. get (key)));
125 let wooco = COMPOSITE_META . some (( key ) => present (meta. get (key)));
126 const type = typeof record?.type === 'string' ? record.type : null ;
127 if (type && (woosb || wooco) && ! ((woosb && type === 'woosb' ) || (wooco && type === 'composite' ))) {
128 return { hasBundleFields: false , bundle: null , unreadableReason: null , staleBundleMeta: true };
129 }
130 if (type === 'woosb' ) wooco = false ;
131 if ( ! woosb && ! wooco) return { hasBundleFields: false , bundle: null , unreadableReason: null };
132 try {
133 if (woosb) {
134 const optional = truthy (meta. get ( 'woosb_optional_products' ));
135 return {
136 hasBundleFields: true ,
137 unreadableReason: null ,
138 bundle: {
139 kind: 'fixed' ,
140 components: woosbComponents (meta. get ( 'woosb_ids' )). map (( part ) => ({ ... part, optional: optional || part.optional })),
141 groups: [],
142 priceRule: truthy (meta. get ( 'woosb_disable_auto_price' )) ? 'fixed' : 'derived' ,
143 stockRule: stockRuleOf (record),
144 detectedBy: 'wordpress:woo-product-bundle' ,
145 },
146 };
147 }
148 const pricing = String (meta. get ( 'wooco_pricing' ) ?? '' ). trim ();
149 return {
150 hasBundleFields: true ,
151 unreadableReason: null ,
152 bundle: {
153 kind: 'configurable' ,
154 components: [],
155 groups: woocoGroups (meta. get ( 'wooco_components' ), { productsForRule }),
156 priceRule: pricing === 'only' ? 'fixed' : pricing ? 'derived' : 'unknown' ,
157 stockRule: stockRuleOf (record),
158 detectedBy: 'wordpress:wpc-composite-products' ,
159 },
160 };
161 } catch (error) {
162 return { hasBundleFields: true , bundle: null , unreadableReason: error.message };
163 }
164 }
165
166 module . exports = { CARRIED_META_KEYS, canonicalBundleFromWooProduct };