← back to Dw Signup Fulfillment

lib/giftcode-discount.js

85 lines

'use strict';
// ALTERNATE — NOT WIRED. The retail default is lib/retail-code.js (function-backed
// unique single-use sample code). This price-rule + collection-scoped discount path
// is kept for reference only (reachable via POST /admin/retail/issue?mode=discount).
// It relies on a Samples COLLECTION to scope the 100%-off; because DW samples share
// a product with the sellable roll variant, collection scoping is riskier than the
// function's variant-level targeting used by retail-code.js.
//
// Instead of a stored-value gift card, this creates a UNIQUE, SINGLE-USE,
// 100%-off discount code SCOPED to the Samples product set. Compared to the
// gift-card path this ENFORCES the "3 samples" intent more tightly:
//   - target = the Samples collection only (can't be spent on full rolls),
//   - value  = 100% off,
//   - usage_limit = 1 (single use),
//   - prerequisite_quantity_range caps the number of sample items to
//     FREE_SAMPLE_COUNT so the code covers exactly N samples, not the whole cart.
//
// Shopify models this as a price_rule + a discount_code under it.
//
// DRY_RUN-safe: both POSTs are short-circuited by lib/shopify.js.
//
// NOTE: requires SAMPLES_COLLECTION_ID (env). If unset, we still build + log the
// full intended payloads and mark a TODO — Steve fills the collection id at go-live.
const config = require('./config');
const shopify = require('./shopify');
const email = require('./email');

function uniqueCode() {
  return 'DWSAMPLES-' + Math.random().toString(36).slice(2, 8).toUpperCase();
}

async function issueRetailDiscountCode(customer) {
  const code = uniqueCode();
  const collectionId = config.SAMPLES_COLLECTION_ID || null;

  const pricePayload = {
    price_rule: {
      title: `Free ${config.FREE_SAMPLE_COUNT} samples — ${customer.email || customer.id}`,
      target_type: 'line_item',
      target_selection: 'entitled',
      allocation_method: 'across',
      value_type: 'percentage',
      value: '-100.0',
      customer_selection: 'all',
      // scope to the Samples collection when we have the id
      entitled_collection_ids: collectionId ? [Number(collectionId)] : [],
      // cap to N sample items
      prerequisite_quantity_range: { greater_than_or_equal_to: 1 },
      allocation_limit: config.FREE_SAMPLE_COUNT,
      usage_limit: 1,
      once_per_customer: true,
      starts_at: new Date().toISOString(),
    },
  };

  const todo = collectionId ? null
    : 'TODO(go-live): set SAMPLES_COLLECTION_ID so the code is scoped to the Samples collection; empty means it is NOT scoped yet.';

  const pr = await shopify.request('POST', '/price_rules.json', pricePayload);
  const priceRuleId = pr.json && pr.json.price_rule ? pr.json.price_rule.id : null;

  const dcPayload = { discount_code: { code } };
  const dc = await shopify.request('POST', `/price_rules/${priceRuleId}/discount_codes.json`, dcPayload);
  const finalCode = (dc.json && dc.json.discount_code && dc.json.discount_code.code) || code;

  const firstName = customer.first_name || (customer.email ? customer.email.split('@')[0] : '');
  const tpl = email.retailGiftEmail({ firstName, code: finalCode, value: config.SAMPLE_GIFT_VALUE, count: config.FREE_SAMPLE_COUNT });
  const mail = await email.sendEmail({ to: customer.email, subject: tpl.subject, html: tpl.html, source: 'retail-discount' });

  return {
    path: 'discount_code',
    code: finalCode,
    priceRuleId,
    scopedToCollection: collectionId,
    todo,
    email: { to: customer.email, subject: tpl.subject, dryRun: mail.dryRun || false },
    shopifyCalls: [
      pr.dryRun ? { WOULD: `${pr.method} ${pr.url}`, payload: pr.body } : { status: pr.status },
      dc.dryRun ? { WOULD: `${dc.method} ${dc.url}`, payload: dc.body } : { status: dc.status },
    ],
  };
}

module.exports = { issueRetailDiscountCode };