@blamejs/blamejs-shop 0.0.52 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/email.js CHANGED
@@ -134,6 +134,103 @@ var REFUND_TEXT =
134
134
  "We've issued a refund of {{amount_formatted}} against order {{order_id}}.\n" +
135
135
  "The funds will appear on your statement within 5-10 business days.\n";
136
136
 
137
+ // Wishlist discount — a watched product just dropped in price.
138
+ // Brand tokens: #0d0d0d ink, #fa4f09 accent, #ffffff paper.
139
+
140
+ var WISHLIST_DISCOUNT_HTML =
141
+ "<!DOCTYPE html>\n" +
142
+ "<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Price drop on {{product_title}}</title></head>" +
143
+ "<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
144
+ "<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
145
+ " <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Price drop on your wishlist</h1>\n" +
146
+ " <p style=\"margin:0 0 16px;\"><strong>{{product_title}}</strong> just dropped to <strong style=\"color:#fa4f09;\">{{new_price}}</strong> (was <s>{{old_price}}</s>) — that's <strong>{{discount_pct}}% off</strong>.</p>\n" +
147
+ " <p style=\"margin:0 0 16px;\">Expires: {{expires_at}}</p>\n" +
148
+ " <p style=\"margin:24px 0;\"><a href=\"{{product_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">Buy now</a></p>\n" +
149
+ " <p style=\"margin:0;color:#0d0d0d;font-size:13px;\">Link: {{product_url}}</p>\n" +
150
+ "</div>\n" +
151
+ "</body></html>\n";
152
+
153
+ var WISHLIST_DISCOUNT_TEXT =
154
+ "Price drop on your wishlist\n\n" +
155
+ "{{product_title}} just dropped to {{new_price}} (was {{old_price}}) — {{discount_pct}}% off.\n" +
156
+ "Expires: {{expires_at}}\n\n" +
157
+ "Buy now: {{product_url}}\n";
158
+
159
+ // Abandoned cart — items still in cart 24h+ later.
160
+ //
161
+ // Loop templates ({{var}}-rendered once per line, then joined) live
162
+ // alongside the outer document. The outer document is split into
163
+ // `_BEFORE_LINES` + `_AFTER_LINES` so the assembled (already-safe)
164
+ // line HTML can be concatenated between two `_render()` halves —
165
+ // the strict renderer keeps escaping every substituted value at
166
+ // both ends, no raw HTML ever flows through a {{var}} slot.
167
+
168
+ var ABANDONED_CART_HTML_BEFORE_LINES =
169
+ "<!DOCTYPE html>\n" +
170
+ "<html lang=\"en\"><head><meta charset=\"utf-8\"><title>You left something behind</title></head>" +
171
+ "<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
172
+ "<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
173
+ " <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">You left something behind</h1>\n" +
174
+ " <p style=\"margin:0 0 16px;\">Hi {{customer_name}}, your cart is still waiting:</p>\n" +
175
+ " <table border=\"0\" cellpadding=\"6\" cellspacing=\"0\" style=\"border-collapse:collapse;width:100%;color:#0d0d0d;\">\n";
176
+
177
+ var ABANDONED_CART_HTML_LINE =
178
+ " <tr><td>{{title}} &times; {{qty}}</td><td align=\"right\">{{price}}</td></tr>\n";
179
+
180
+ var ABANDONED_CART_HTML_AFTER_LINES =
181
+ " <tr><td style=\"border-top:2px solid #0d0d0d;\"><strong>Total</strong></td><td align=\"right\" style=\"border-top:2px solid #0d0d0d;\"><strong>{{total}}</strong></td></tr>\n" +
182
+ " </table>\n" +
183
+ " <p style=\"margin:16px 0;color:#0d0d0d;\">{{notes}}</p>\n" +
184
+ " <p style=\"margin:24px 0;\"><a href=\"{{cart_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">Return to cart</a></p>\n" +
185
+ "</div>\n" +
186
+ "</body></html>\n";
187
+
188
+ var ABANDONED_CART_TEXT_BEFORE_LINES =
189
+ "You left something behind\n\n" +
190
+ "Hi {{customer_name}}, your cart is still waiting:\n\n";
191
+
192
+ var ABANDONED_CART_TEXT_LINE =
193
+ " {{title}} x {{qty}} — {{price}}\n";
194
+
195
+ var ABANDONED_CART_TEXT_AFTER_LINES =
196
+ "-------------------------------------\n" +
197
+ "Total: {{total}}\n\n" +
198
+ "{{notes}}\n\n" +
199
+ "Return to cart: {{cart_url}}\n";
200
+
201
+ // Review request — sent ~7 days after ship. Per-product review
202
+ // links use the same before/lines/after split as the abandoned
203
+ // cart so the strict renderer keeps its escape-everything property.
204
+
205
+ var REVIEW_REQUEST_HTML_BEFORE_LINES =
206
+ "<!DOCTYPE html>\n" +
207
+ "<html lang=\"en\"><head><meta charset=\"utf-8\"><title>How was your order?</title></head>" +
208
+ "<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
209
+ "<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
210
+ " <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">How was your order?</h1>\n" +
211
+ " <p style=\"margin:0 0 16px;\">Hi {{customer_name}}, hope you're enjoying order <strong>{{order_id}}</strong>. A quick review helps the next customer:</p>\n" +
212
+ " <ul style=\"padding-left:20px;margin:0 0 16px;color:#0d0d0d;\">\n";
213
+
214
+ var REVIEW_REQUEST_HTML_LINE =
215
+ " <li><a href=\"{{review_url}}\" style=\"color:#fa4f09;\">Review {{title}}</a></li>\n";
216
+
217
+ var REVIEW_REQUEST_HTML_AFTER_LINES =
218
+ " </ul>\n" +
219
+ " <p style=\"margin:16px 0;color:#0d0d0d;font-size:13px;\">Each link takes about a minute. Thank you.</p>\n" +
220
+ "</div>\n" +
221
+ "</body></html>\n";
222
+
223
+ var REVIEW_REQUEST_TEXT_BEFORE_LINES =
224
+ "How was your order?\n\n" +
225
+ "Hi {{customer_name}}, hope you're enjoying order {{order_id}}.\n" +
226
+ "A quick review helps the next customer:\n\n";
227
+
228
+ var REVIEW_REQUEST_TEXT_LINE =
229
+ " Review {{title}}: {{review_url}}\n";
230
+
231
+ var REVIEW_REQUEST_TEXT_AFTER_LINES =
232
+ "\nEach link takes about a minute. Thank you.\n";
233
+
137
234
  // ---- factory ------------------------------------------------------------
138
235
 
139
236
  function _email(s) {
@@ -221,6 +318,159 @@ function create(opts) {
221
318
  var text = _render(REFUND_TEXT, vars);
222
319
  return await _send(input.customer.email, "Refund issued — " + input.order.id, html, text, input.replyTo);
223
320
  },
321
+
322
+ // Wishlist discount — a watched product just dropped in price.
323
+ // Operator passes already-formatted `old_price` / `new_price`
324
+ // strings; pricing rules vary (locale, multi-currency, promo
325
+ // overlay) and email shouldn't re-derive them. `expires_at` is
326
+ // optional — defaults to "no expiry" so the template's required
327
+ // slot still receives an escaped string.
328
+ sendWishlistDiscount: async function (input) {
329
+ if (!input) throw new TypeError("email.sendWishlistDiscount: input object required");
330
+ if (typeof input.customer_email !== "string" || !input.customer_email) {
331
+ throw new TypeError("email.sendWishlistDiscount: customer_email required");
332
+ }
333
+ if (typeof input.product_title !== "string" || !input.product_title) {
334
+ throw new TypeError("email.sendWishlistDiscount: product_title required");
335
+ }
336
+ if (typeof input.product_url !== "string" || !input.product_url) {
337
+ throw new TypeError("email.sendWishlistDiscount: product_url required");
338
+ }
339
+ var vars = {
340
+ product_title: input.product_title,
341
+ product_url: input.product_url,
342
+ old_price: input.old_price == null ? "—" : String(input.old_price),
343
+ new_price: input.new_price == null ? "—" : String(input.new_price),
344
+ discount_pct: input.discount_pct == null ? "—" : String(input.discount_pct),
345
+ expires_at: input.expires_at ? String(input.expires_at) : "no expiry",
346
+ };
347
+ var html = _render(WISHLIST_DISCOUNT_HTML, vars);
348
+ var text = _render(WISHLIST_DISCOUNT_TEXT, vars);
349
+ return await _send(
350
+ input.customer_email,
351
+ "Price drop on " + input.product_title,
352
+ html, text, input.replyTo
353
+ );
354
+ },
355
+
356
+ // Abandoned cart — items still in cart 24h+ later. The line set
357
+ // is rendered through the strict renderer once per line so each
358
+ // line's `title` / `qty` / `price` is HTML-escaped independently,
359
+ // then concatenated between the BEFORE/AFTER halves. No raw HTML
360
+ // ever flows through a {{var}} slot.
361
+ sendAbandonedCartReminder: async function (input) {
362
+ if (!input) throw new TypeError("email.sendAbandonedCartReminder: input object required");
363
+ if (typeof input.customer_email !== "string" || !input.customer_email) {
364
+ throw new TypeError("email.sendAbandonedCartReminder: customer_email required");
365
+ }
366
+ if (typeof input.cart_url !== "string" || !input.cart_url) {
367
+ throw new TypeError("email.sendAbandonedCartReminder: cart_url required");
368
+ }
369
+ if (!Array.isArray(input.lines) || input.lines.length === 0) {
370
+ throw new TypeError("email.sendAbandonedCartReminder: lines array required");
371
+ }
372
+ var i;
373
+ for (i = 0; i < input.lines.length; i += 1) {
374
+ var ln = input.lines[i];
375
+ if (!ln || typeof ln !== "object") {
376
+ throw new TypeError("email.sendAbandonedCartReminder: lines[" + i + "] must be an object");
377
+ }
378
+ if (typeof ln.title !== "string" || !ln.title) {
379
+ throw new TypeError("email.sendAbandonedCartReminder: lines[" + i + "].title required");
380
+ }
381
+ }
382
+ var headerVars = { customer_name: input.customer_name || "there" };
383
+ var footerVars = {
384
+ total: input.total == null ? "—" : String(input.total),
385
+ notes: input.notes || "",
386
+ cart_url: input.cart_url,
387
+ };
388
+ var htmlLines = "";
389
+ var textLines = "";
390
+ for (i = 0; i < input.lines.length; i += 1) {
391
+ var lineVars = {
392
+ title: input.lines[i].title,
393
+ qty: input.lines[i].qty == null ? "—" : String(input.lines[i].qty),
394
+ price: input.lines[i].price == null ? "—" : String(input.lines[i].price),
395
+ };
396
+ htmlLines += _render(ABANDONED_CART_HTML_LINE, lineVars);
397
+ textLines += _render(ABANDONED_CART_TEXT_LINE, lineVars);
398
+ }
399
+ var html =
400
+ _render(ABANDONED_CART_HTML_BEFORE_LINES, headerVars) +
401
+ htmlLines +
402
+ _render(ABANDONED_CART_HTML_AFTER_LINES, footerVars);
403
+ var text =
404
+ _render(ABANDONED_CART_TEXT_BEFORE_LINES, headerVars) +
405
+ textLines +
406
+ _render(ABANDONED_CART_TEXT_AFTER_LINES, footerVars);
407
+ return await _send(
408
+ input.customer_email,
409
+ "You left something behind",
410
+ html, text, input.replyTo
411
+ );
412
+ },
413
+
414
+ // Review request — sent ~7 days after ship. Per-product review
415
+ // links are derived from review_base_url + "/" + slug + "/review",
416
+ // then HTML-escaped through the strict renderer with the rest of
417
+ // the per-line substitutions.
418
+ sendReviewRequest: async function (input) {
419
+ if (!input) throw new TypeError("email.sendReviewRequest: input object required");
420
+ if (typeof input.customer_email !== "string" || !input.customer_email) {
421
+ throw new TypeError("email.sendReviewRequest: customer_email required");
422
+ }
423
+ if (typeof input.order_id !== "string" || !input.order_id) {
424
+ throw new TypeError("email.sendReviewRequest: order_id required");
425
+ }
426
+ if (!Array.isArray(input.products) || input.products.length === 0) {
427
+ throw new TypeError("email.sendReviewRequest: products array required");
428
+ }
429
+ if (typeof input.review_base_url !== "string" || !input.review_base_url) {
430
+ throw new TypeError("email.sendReviewRequest: review_base_url required");
431
+ }
432
+ var i;
433
+ for (i = 0; i < input.products.length; i += 1) {
434
+ var p = input.products[i];
435
+ if (!p || typeof p !== "object") {
436
+ throw new TypeError("email.sendReviewRequest: products[" + i + "] must be an object");
437
+ }
438
+ if (typeof p.title !== "string" || !p.title) {
439
+ throw new TypeError("email.sendReviewRequest: products[" + i + "].title required");
440
+ }
441
+ if (typeof p.slug !== "string" || !p.slug) {
442
+ throw new TypeError("email.sendReviewRequest: products[" + i + "].slug required");
443
+ }
444
+ }
445
+ var headerVars = {
446
+ customer_name: input.customer_name || "there",
447
+ order_id: input.order_id,
448
+ };
449
+ var htmlLines = "";
450
+ var textLines = "";
451
+ for (i = 0; i < input.products.length; i += 1) {
452
+ var pr = input.products[i];
453
+ var lineVars = {
454
+ title: pr.title,
455
+ review_url: input.review_base_url + "/" + pr.slug + "/review",
456
+ };
457
+ htmlLines += _render(REVIEW_REQUEST_HTML_LINE, lineVars);
458
+ textLines += _render(REVIEW_REQUEST_TEXT_LINE, lineVars);
459
+ }
460
+ var html =
461
+ _render(REVIEW_REQUEST_HTML_BEFORE_LINES, headerVars) +
462
+ htmlLines +
463
+ REVIEW_REQUEST_HTML_AFTER_LINES;
464
+ var text =
465
+ _render(REVIEW_REQUEST_TEXT_BEFORE_LINES, headerVars) +
466
+ textLines +
467
+ REVIEW_REQUEST_TEXT_AFTER_LINES;
468
+ return await _send(
469
+ input.customer_email,
470
+ "How was your order " + input.order_id + "?",
471
+ html, text, input.replyTo
472
+ );
473
+ },
224
474
  };
225
475
  }
226
476
 
@@ -238,5 +488,19 @@ module.exports = {
238
488
  SHIP_NOTIFICATION_TEXT: SHIP_NOTIFICATION_TEXT,
239
489
  REFUND_HTML: REFUND_HTML,
240
490
  REFUND_TEXT: REFUND_TEXT,
491
+ WISHLIST_DISCOUNT_HTML: WISHLIST_DISCOUNT_HTML,
492
+ WISHLIST_DISCOUNT_TEXT: WISHLIST_DISCOUNT_TEXT,
493
+ ABANDONED_CART_HTML_BEFORE_LINES: ABANDONED_CART_HTML_BEFORE_LINES,
494
+ ABANDONED_CART_HTML_LINE: ABANDONED_CART_HTML_LINE,
495
+ ABANDONED_CART_HTML_AFTER_LINES: ABANDONED_CART_HTML_AFTER_LINES,
496
+ ABANDONED_CART_TEXT_BEFORE_LINES: ABANDONED_CART_TEXT_BEFORE_LINES,
497
+ ABANDONED_CART_TEXT_LINE: ABANDONED_CART_TEXT_LINE,
498
+ ABANDONED_CART_TEXT_AFTER_LINES: ABANDONED_CART_TEXT_AFTER_LINES,
499
+ REVIEW_REQUEST_HTML_BEFORE_LINES: REVIEW_REQUEST_HTML_BEFORE_LINES,
500
+ REVIEW_REQUEST_HTML_LINE: REVIEW_REQUEST_HTML_LINE,
501
+ REVIEW_REQUEST_HTML_AFTER_LINES: REVIEW_REQUEST_HTML_AFTER_LINES,
502
+ REVIEW_REQUEST_TEXT_BEFORE_LINES: REVIEW_REQUEST_TEXT_BEFORE_LINES,
503
+ REVIEW_REQUEST_TEXT_LINE: REVIEW_REQUEST_TEXT_LINE,
504
+ REVIEW_REQUEST_TEXT_AFTER_LINES: REVIEW_REQUEST_TEXT_AFTER_LINES,
241
505
  },
242
506
  };