← 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 };