Setting the file. One moment.
Shopify Gift Card Remedy · Rp Source Shopify · wix/skills · Skills Docs
ContentsBack to the top of the page 157
async function configuredStore
— line 157
This file
Number 38.3
Position 3 of 51
Type JavaScript
Size 10 KB
Lines 191 scripts/ shopify-gift-card-remedy.js
JavaScript · 191 lines · 10 KB
10 const { SHOPIFY_SOURCE_ENV } = require ( '../../../lib/source-access.js' );
11 const { writeJsonAtomic } = require ( '../../../lib/source-coverage.js' );
12 const { createStoreExecutor } = require ( '../lib/shopify-cli.js' );
13 const session = require ( '../lib/shopify-session.js' );
14 const gift = require ( '../lib/shopify-gift-cards.js' );
15
16 const COLUMNS = Object. freeze ([ 'id' , 'wixGiftCardId' , 'replacementAmount' , 'currency' , 'holderNotifiedOn' , 'notificationConfirmedBy' ]);
17 const USAGE = 'Usage: shopify-gift-card-remedy.js <export|activity-stopped|record-replacements|keep-redeeming> --project <project> [--at <ISO> | --evidence <file.csv> | --until <YYYY-MM-DD>] [--by <name>] \n ' ;
18
19 function parseArgs ( argv ) {
20 const out = { command: argv[ 0 ] || null , project: null , at: null , evidence: null , until: null , by: null , help: false };
21 for ( let i = 1 ; i < argv. length ; i += 1 ) {
22 const arg = argv[i];
23 if (arg === '--help' || arg === '-h' ) out.help = true ;
24 else if ([ '--project' , '--at' , '--evidence' , '--until' , '--by' ]. includes (arg)) { out[arg. slice ( 2 )] = argv[i + 1 ]; i += 1 ; }
25 else throw new Error ( `unknown argument ${ arg }` );
26 }
27 return out;
28 }
29
30 function parseCsv ( text ) {
31 const rows = [];
32 let row = [];
33 let field = '' ;
34 let quoted = false ;
35 for ( let i = 0 ; i < text. length ; i += 1 ) {
36 const ch = text[i];
37 if (quoted) {
38 if (ch === '"' && text[i + 1 ] === '"' ) { field += '"' ; i += 1 ; }
39 else if (ch === '"' ) quoted = false ;
40 else field += ch;
41 } else if (ch === '"' ) quoted = true ;
42 else if (ch === ',' ) { row. push (field); field = '' ; }
43 else if (ch === ' \n ' ) { row. push (field. replace ( / \r $ / , '' )); rows. push (row); row = []; field = '' ; }
44 else field += ch;
45 }
46 if (quoted) throw new Error ( 'CSV has an unterminated quoted field' );
47 if (field. length || row. length ) { row. push (field. replace ( / \r $ / , '' )); rows. push (row); }
48 return rows. filter (( entry ) => entry. some (( value ) => value !== '' ));
49 }
50
51 function validDate ( value ) {
52 if ( ! / ^ \d {4} - \d {2} - \d {2}$ / . test ( String (value || '' ))) return false ;
53 const date = new Date ( `${ value }T00:00:00.000Z` );
54 return Number. isFinite (date. getTime ()) && date. toISOString (). slice ( 0 , 10 ) === value;
55 }
56
57 function csvCell ( value ) {
58 const text = value === null || value === undefined ? '' : String (value);
59 return / [",\r\n] / . test (text) ? `"${ text . replace ( /"/ g , '""' ) }"` : text;
60 }
61
62 async function readRemedy ( projectDir ) {
63 return gift. readJson (path. join (projectDir, gift. REMEDY_FILE ), { replacements: [] });
64 }
65
66 async function writeRemedy ( projectDir , value ) {
67 await writeJsonAtomic (path. join (projectDir, gift. REMEDY_FILE ), value);
68 }
69
70 async function recordActivityStopped ( projectDir , { at , by , now = () => new Date (). toISOString () } = {}) {
71 if ( ! Number. isFinite (Date. parse (at))) throw new Error ( '--at must be an ISO timestamp' );
72 if ( ! by) throw new Error ( '--by is required' );
73 const record = { activityStoppedAt: new Date (at). toISOString (), recordedBy: by, recordedAt: now () };
74 await writeJsonAtomic (path. join (projectDir, gift. ACTIVITY_FILE ), record);
75 return record;
76 }
77
78 async function shopTimezone ( execute ) {
79 const result = await execute ( 'query RpGiftCardShopTimezone { shop { ianaTimezone } }' );
80 if (result.errors && result.errors. length ) throw new Error ( `Shopify shop timezone could not be read: ${ result . errors . map (( error ) => error . message ). join ( '; ' ) }` );
81 const timezone = result.data && result.data.shop && result.data.shop.ianaTimezone;
82 if ( typeof timezone !== 'string' || ! timezone) throw new Error ( 'Shopify shop timezone was missing' );
83 // Make Intl validate the IANA identifier now, before changing any file.
84 gift. dateInTimezone ( new Date (), timezone);
85 return timezone;
86 }
87
88 async function recordReplacements ( projectDir , { evidence , by , execute , now = () => new Date (). toISOString () } = {}) {
89 if ( ! evidence) throw new Error ( '--evidence is required' );
90 if ( ! by) throw new Error ( '--by is required' );
91 if ( typeof execute !== 'function' ) throw new Error ( 'a Shopify executor is required' );
92 const timezone = await shopTimezone (execute);
93 const today = gift. dateInTimezone ( new Date ( now ()), timezone);
94 const rows = parseCsv ( await fs. readFile (evidence, 'utf8' ));
95 if ( ! rows. length || rows[ 0 ]. length !== COLUMNS . length || ! rows[ 0 ]. every (( value , index ) => value === COLUMNS [index])) {
96 throw new Error ( `CSV columns must be exactly ${ COLUMNS . join ( ',' ) }` );
97 }
98 const { latest } = await gift. giftCardReads (projectDir);
99 if ( ! latest) throw new Error ( 'no successful gift-card read exists' );
100 const cards = new Map (gift. summarizeCards (latest.cards).owed. map (( card ) => [card.id, card]));
101 const seen = new Set ();
102 const replacements = [];
103 for ( let index = 1 ; index < rows. length ; index += 1 ) {
104 if (rows[index]. length !== COLUMNS . length ) throw new Error ( `CSV row ${ index + 1 } has ${ rows [ index ]. length } columns, expected ${ COLUMNS . length }` );
105 const candidate = Object. fromEntries ( COLUMNS . map (( column , at ) => [column, rows[index][at]]));
106 const card = cards. get (candidate.id);
107 if ( ! card) throw new Error ( `CSV row ${ index + 1 }: id is not an owed card in the latest read` );
108 if (seen. has (candidate.id)) throw new Error ( `CSV row ${ index + 1 }: duplicate id ${ candidate . id }` );
109 seen. add (candidate.id);
110 if ( ! candidate.wixGiftCardId || ! candidate.holderNotifiedOn || ! candidate.notificationConfirmedBy) throw new Error ( `CSV row ${ index + 1 }: Wix card id and holder-notification evidence are required` );
111 if ( ! validDate (candidate.holderNotifiedOn) || candidate.holderNotifiedOn > today) throw new Error ( `CSV row ${ index + 1 }: holderNotifiedOn must be a valid date no later than today in ${ timezone }` );
112 const amount = gift. decimal (candidate.replacementAmount);
113 if (amount.units <= 0 n ) throw new Error ( `CSV row ${ index + 1 }: replacementAmount must be above zero` );
114 if (candidate.currency !== card.balance.currencyCode) throw new Error ( `CSV row ${ index + 1 }: currency must be ${ card . balance . currencyCode }` );
115 replacements. push ({ ... candidate, recordedBy: by, recordedAt: now () });
116 }
117 if ( ! replacements. length ) throw new Error ( 'CSV must contain at least one owed-card replacement' );
118 const remedy = await readRemedy (projectDir);
119 const merged = new Map (((remedy && remedy.replacements) || []). map (( row ) => [row.id, row]));
120 replacements. forEach (( row ) => merged. set (row.id, row));
121 const next = { ... remedy, ianaTimezone: timezone, replacements: [ ... merged. values ()]. sort (( a , b ) => a.id. localeCompare (b.id)) };
122 await writeRemedy (projectDir, next);
123 return next;
124 }
125
126 async function keepRedeeming ( projectDir , { until , by , execute , now = () => new Date (). toISOString () } = {}) {
127 if ( ! validDate (until)) throw new Error ( '--until must be a YYYY-MM-DD calendar date' );
128 if ( ! by) throw new Error ( '--by is required' );
129 if ( typeof execute !== 'function' ) throw new Error ( 'a Shopify executor is required' );
130 const timezone = await shopTimezone (execute);
131 const remedy = await readRemedy (projectDir);
132 const next = { ... remedy, replacements: Array. isArray (remedy.replacements) ? remedy.replacements : [], keepRedeemingUntil: until, keepRedeemingRecordedBy: by, keepRedeemingRecordedAt: now (), ianaTimezone: timezone };
133 await writeRemedy (projectDir, next);
134 return next;
135 }
136
137 async function exportOwed ( projectDir ) {
138 const { latest } = await gift. giftCardReads (projectDir);
139 if ( ! latest) throw new Error ( 'no successful gift-card read exists' );
140 const summary = gift. summarizeCards (latest.cards);
141 const state = await gift. evaluateGiftCardCutover (projectDir);
142 const uncleared = new Set ((state.uncleared || summary.owed). map (( card ) => card.id));
143 const lines = [[ 'id' , 'lastCharacters' , 'balance' , 'currency' , 'enabled' , 'expiresOn' , 'replaced' ]];
144 for ( const card of summary.owed) {
145 const replaced = state.finalRead && ! uncleared. has (card.id) ? 'yes' : 'no' ;
146 lines. push ([card.id, card.lastCharacters || '' , card.balance.amount, card.balance.currencyCode, card.enabled ? 'true' : 'false' , card.expiresOn || '' , replaced]);
147 }
148 const text = `${ lines . map (( row ) => row . map ( csvCell ). join ( ',' )). join ( ' \n ' ) } \n ` ;
149 const file = path. join (projectDir, gift. OWED_CSV_FILE );
150 await fs. mkdir (path. dirname (file), { recursive: true });
151 const temp = `${ file }.${ process . pid }.${ Date . now () }.tmp` ;
152 await fs. writeFile (temp, text, 'utf8' );
153 await fs. rename (temp, file);
154 return { file: gift. OWED_CSV_FILE , rows: summary.owed. length };
155 }
156
157 async function configuredStore ( projectDir ) {
158 const values = await readEnvFile (path. join (projectDir, SHOPIFY_SOURCE_ENV ));
159 if ( ! values. SHOPIFY_SHOP_DOMAIN ) throw new Error ( `SHOPIFY_SHOP_DOMAIN is missing from ${ SHOPIFY_SOURCE_ENV }` );
160 return session. normalizeDomain (values. SHOPIFY_SHOP_DOMAIN );
161 }
162
163 async function main ( argv = process.argv. slice ( 2 )) {
164 let args;
165 try { args = parseArgs (argv); } catch (error) { process.stderr. write ( `${ error . message } \n ${ USAGE }` ); return 1 ; }
166 if (args.help) { process.stdout. write ( USAGE ); return 0 ; }
167 if ( ! args.project || ! [ 'export' , 'activity-stopped' , 'record-replacements' , 'keep-redeeming' ]. includes (args.command)) { process.stderr. write ( USAGE ); return 1 ; }
168 const projectDir = path. resolve (args.project);
169 try {
170 let result;
171 if (args.command === 'export' ) result = await exportOwed (projectDir);
172 else if (args.command === 'activity-stopped' ) result = await recordActivityStopped (projectDir, args);
173 else {
174 const store = await configuredStore (projectDir);
175 await session. assertConnectionReady (projectDir, store);
176 const execute = createStoreExecutor ({ store });
177 result = args.command === 'record-replacements'
178 ? await recordReplacements (projectDir, { ... args, execute })
179 : await keepRedeeming (projectDir, { ... args, execute });
180 }
181 process.stdout. write ( `${ JSON . stringify ({ ok: true , command: args.command , ... result }) } \n ` );
182 return 0 ;
183 } catch (error) {
184 process.stdout. write ( `${ JSON . stringify ({ ok: false , command: args.command , code: error.code || 'shopify_gift_card_remedy_invalid' , message: error.message }) } \n ` );
185 return 2 ;
186 }
187 }
188
189 if (require.main === module ) main (). then (( code ) => { process.exitCode = code; }). catch (( error ) => { process.stderr. write ( `${ error . stack || error . message } \n ` ); process.exitCode = 1 ; });
190
191 module . exports = { COLUMNS, parseArgs, parseCsv, validDate, csvCell, recordActivityStopped, shopTimezone, recordReplacements, keepRedeeming, exportOwed, main };