← back to Whatsmystyle
scripts/resale-guardrails.js
101 lines
/**
* Resale guardrails — scaffold only, no resale UX shipped yet.
*
* Per the 2026-05-12 dream-team verdict on "should WhatsMyStyle help
* users resell closet items": HOLD until three guardrails are in place.
*
* 1. Circularity check — an item is eligible for a resale nudge only if:
* - it's been in the closet ≥ 90 days, OR
* - the user has confirmed "I've worn this >5 times", OR
* - the user has confirmed "this no longer fits/I will not wear again"
* Goal: prevent the app from accelerating fast-fashion churn.
*
* 2. Repair / recycle FIRST — before showing any sell-this CTA, the user
* must dismiss two options:
* a) repair_link — link to a local tailor/cobbler (Google Maps
* Places query "tailor near me" or "cobbler near me")
* b) recycle_link — link to a textile take-back program
* (Patagonia Worn Wear, Eileen Fisher Renew, For Days Take-Back,
* local Goodwill / textile recycle dropoff)
* Only after the user dismisses BOTH do we surface the sell CTA.
*
* 3. Privacy limits — resale events are EPHEMERAL. We don't write to
* outfit_picks, we don't write to duels, we don't update the taste
* vector. We log only the local-time + closet_id + action verb to
* data/resale-events.jsonl for the user's own export — never the
* server's analytical use.
*
* Until the UX ships, this module just exposes the eligibility check +
* resource links so the rest of the codebase can reason about it.
*/
const fs = require('fs');
const path = require('path');
const DATA_DIR = path.join(__dirname, '..', 'data');
const EPHEMERAL_LOG = path.join(DATA_DIR, 'resale-events.jsonl');
// --- Guardrail 1: circularity ---------------------------------------------
function isEligibleForResaleNudge(closetItem, userConfirmation = {}) {
const ageMs = Date.now() - Date.parse(closetItem.created_at || closetItem.acquired_at || 0);
const ageDays = ageMs / (24 * 60 * 60 * 1000);
if (ageDays >= 90) return { ok: true, reason: 'age>=90d' };
if (userConfirmation.worn_more_than_5) return { ok: true, reason: 'user-confirmed worn 5+' };
if (userConfirmation.no_longer_fits) return { ok: true, reason: 'user-confirmed no longer fits' };
if (userConfirmation.never_will_wear) return { ok: true, reason: 'user-confirmed will not wear' };
return { ok: false, reason: `${Math.round(ageDays)}d old + no user confirmation; nudge blocked` };
}
// --- Guardrail 2: repair / recycle ----------------------------------------
// Hand-curated take-back programs. Add to this list as we vet new ones —
// each one MUST be a real program that accepts garments from the public.
const TAKEBACK_PROGRAMS = [
{ name: 'Patagonia Worn Wear', url: 'https://wornwear.patagonia.com/', accepts: 'Patagonia only', kind: 'repair+resell' },
{ name: 'Eileen Fisher Renew', url: 'https://www.eileenfisherrenew.com/', accepts: 'Eileen Fisher only', kind: 'repair+resell' },
{ name: 'Levi\'s SecondHand', url: 'https://secondhand.levi.com/', accepts: "Levi's only", kind: 'resell' },
{ name: 'For Days Take Back', url: 'https://fordays.com/pages/take-back-bag', accepts: 'any brand, any condition', kind: 'recycle' },
{ name: 'TerraCycle (varies)', url: 'https://www.terracycle.com/en-US/', accepts: 'depends on local program', kind: 'recycle' },
{ name: 'Local Goodwill', url: 'https://www.goodwill.org/locator/', accepts: 'any wearable garment', kind: 'donate' },
// ---- v12 expansion 2026-05-12 — +6 vetted programs ----
{ name: 'Madewell Denim Recycling', url: 'https://www.madewell.com/inspo-do-well-denim-recycling-landing.html', accepts: 'any-brand denim → insulation', kind: 'recycle' },
{ name: 'REI Used Gear', url: 'https://www.rei.com/used', accepts: 'REI-purchased outdoor/active', kind: 'resell' },
{ name: 'ThredUp Cleanout Kit', url: 'https://www.thredup.com/cleanout', accepts: 'any women\'s+kids brand', kind: 'resell' },
{ name: 'The North Face Renewed', url: 'https://www.thenorthfacerenewed.com/', accepts: 'TNF only', kind: 'repair+resell' },
{ name: 'Nike Reuse-A-Shoe', url: 'https://www.nike.com/sustainability/reuse-a-shoe', accepts: 'any athletic shoe → court material', kind: 'recycle' },
{ name: 'H&M Garment Collecting', url: 'https://www2.hm.com/en_us/sustainability-at-hm/our-work/close-the-loop.html', accepts: 'any brand, any condition (in-store bins)', kind: 'recycle' },
];
function repairAndRecycleOptions({ brand, city } = {}) {
const brandLower = (brand || '').toLowerCase();
const brandMatch = TAKEBACK_PROGRAMS.filter(p => p.accepts.toLowerCase().includes(brandLower) && brandLower);
const general = TAKEBACK_PROGRAMS.filter(p => p.kind === 'recycle' || p.kind === 'donate' || /any brand/.test(p.accepts));
const tailorSearch = city
? `https://www.google.com/maps/search/tailor+near+${encodeURIComponent(city)}`
: 'https://www.google.com/maps/search/tailor+near+me';
const cobblerSearch = city
? `https://www.google.com/maps/search/shoe+repair+near+${encodeURIComponent(city)}`
: 'https://www.google.com/maps/search/shoe+repair+near+me';
return {
repair: { tailor: tailorSearch, cobbler: cobblerSearch },
takeback: [...brandMatch, ...general].slice(0, 5),
};
}
// --- Guardrail 3: ephemeral logging ---------------------------------------
function logResaleEventEphemeral({ user_id, closet_id, action }) {
try {
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.appendFileSync(EPHEMERAL_LOG, JSON.stringify({
ts: new Date().toISOString(),
user_id, closet_id, action,
}) + '\n');
} catch {}
}
module.exports = {
isEligibleForResaleNudge,
repairAndRecycleOptions,
logResaleEventEphemeral,
TAKEBACK_PROGRAMS,
};