Setting the file. One moment.
Customer Member · Rp Target Wix · wix/skills · Skills Docs
ContentsBack to the top of the page 41.10
Multilingual Restore
lib/customer-member.js
lib/ customer-member.js
JavaScript · 104 lines · 6 KB
14 // does not delete its contact) returned the FIRST member's contactId, silently attaching the
15 // second person's name onto the first person's contact record. Two different source customers
16 // who share a phone number (a shared home line, a placeholder store number, a typo) would be
17 // merged into one Wix identity. The caller (wix-writers.js ensureMember) is responsible for
18 // checking Contacts for a phone collision before ever sending a phone on a CREATE.
19
20 const { MEMBER_FINDING , tallyMemberOutcomes } = require ( '../../../lib/member-outcome-registry.js' );
21
22 // Calling code + whether the national number carries a leading trunk '0' to strip, for the
23 // countries this project has needed so far. Deliberately small: an unlisted country REFUSES the
24 // phone (findings, not a guess) rather than emitting a plausible-looking wrong number.
25 const COUNTRY_PHONE_RULES = Object. freeze ({
26 IL: { callingCode: '972' , trunkPrefix: '0' },
27 US: { callingCode: '1' , trunkPrefix: null },
28 CA: { callingCode: '1' , trunkPrefix: null },
29 GB: { callingCode: '44' , trunkPrefix: '0' },
30 FR: { callingCode: '33' , trunkPrefix: '0' },
31 DE: { callingCode: '49' , trunkPrefix: '0' },
32 IT: { callingCode: '39' , trunkPrefix: null },
33 ES: { callingCode: '34' , trunkPrefix: null },
34 AU: { callingCode: '61' , trunkPrefix: '0' },
35 NL: { callingCode: '31' , trunkPrefix: '0' },
36 });
37
38 const E164 = / ^ \+ [1-9]\d {6,14}$ / ;
39
40 // Never invents digits: either the input already carries a confident international shape, or it
41 // converts under an explicit, deterministic rule for a known country, or it is refused outright.
42 function normalizePhoneE164 ( rawPhone , countryCode ) {
43 const raw = rawPhone === undefined || rawPhone === null ? '' : String (rawPhone). trim ();
44 if ( ! raw) return { ok: false , reason: 'empty' };
45 const asIs = raw. replace ( / [\s().-] / g , '' );
46 if ( E164 . test (asIs)) return { ok: true , phone: asIs };
47 const country = countryCode ? String (countryCode). trim (). toUpperCase () : '' ;
48 const rule = COUNTRY_PHONE_RULES [country];
49 if ( ! rule) return { ok: false , reason: country ? 'unsupported-country' : 'no-country-code' };
50 const digits = raw. replace ( / \D / g , '' );
51 if ( ! digits) return { ok: false , reason: 'no-digits' };
52 const national = rule.trunkPrefix && digits. startsWith (rule.trunkPrefix) ? digits. slice (rule.trunkPrefix. length ) : digits;
53 if (national. length < 4 || national. length > 14 ) return { ok: false , reason: 'implausible-length' };
54 const candidate = `+${ rule . callingCode }${ national }` ;
55 if ( ! E164 . test (candidate)) return { ok: false , reason: 'invalid-shape' };
56 return { ok: true , phone: candidate };
57 }
58
59 // A generic customer -> Member.contact (Create Member's shape: firstName, lastName, phones[],
60 // addresses[], no email -- the login email is a sibling field, never here). Findings, not thrown
61 // errors: a field this can't carry is reported and dropped, the rest of the contact still writes.
62 // Trims BEFORE testing for presence, everywhere -- a whitespace-only value (" ") is truthy as a
63 // raw string, and review (2026-09-06) found the field-by-field checks below testing the raw value
64 // first: a source record with a lone space in a name or address field would have written that
65 // space through as if it were real data, rather than being treated as absent.
66 function nonEmpty ( value ) {
67 if (value === null || value === undefined ) return null ;
68 const trimmed = String (value). trim ();
69 return trimmed === '' ? null : trimmed;
70 }
71
72 function buildMemberContact ({ firstName = null , lastName = null , phone = null , countryCode = null , address = null } = {}) {
73 const findings = [];
74 const contact = {};
75 const first = nonEmpty (firstName); if (first) contact.firstName = first;
76 const last = nonEmpty (lastName); if (last) contact.lastName = last;
77 if ( nonEmpty (phone)) {
78 const normalized = normalizePhoneE164 (phone, countryCode);
79 if (normalized.ok) contact.phones = [normalized.phone];
80 else findings. push ({ code: MEMBER_FINDING . PHONE_NOT_NORMALIZED , reason: normalized.reason });
81 }
82 if (address && typeof address === 'object' ) {
83 const a = {};
84 const addressLine = nonEmpty (address.addressLine); if (addressLine) a.addressLine = addressLine;
85 const addressLine2 = nonEmpty (address.addressLine2); if (addressLine2) a.addressLine2 = addressLine2;
86 const city = nonEmpty (address.city); if (city) a.city = city;
87 // Free text: Members V1 stores an unrecognized code as `subdivisionFullname` rather than
88 // rejecting it (verified live), so WooCommerce's state field -- sometimes an ISO subdivision
89 // code, sometimes a free-text region name -- is passed through as-is, never validated here.
90 const subdivision = nonEmpty (address.subdivision); if (subdivision) a.subdivision = subdivision;
91 const country = nonEmpty (address.country); if (country) a.country = country. toUpperCase ();
92 const postalCode = nonEmpty (address.postalCode); if (postalCode) a.postalCode = postalCode;
93 if (Object. keys (a). length > 0 ) contact.addresses = [a];
94 }
95 return { contact, findings };
96 }
97
98 // Mirrors tallyCouponWriteOutcomes / tallyPlanWriteOutcomes's shape. The buckets come from the
99 // member outcome registry, which throws on a code it does not know rather than counting it.
100 function tallyMemberWriteOutcomes ( outcomes = []) {
101 return tallyMemberOutcomes (outcomes);
102 }
103
104 module . exports = { COUNTRY_PHONE_RULES, normalizePhoneE164, buildMemberContact, tallyMemberWriteOutcomes };