From 1d508e426295eb7e340c48fcb02dfbf50bc64508 Mon Sep 17 00:00:00 2001 From: Szamski Date: Sat, 5 Sep 2026 05:59:29 +0200 Subject: [PATCH] feat(legal): guarantee a 14-day refund window for Direct purchases Paddle's risk review is holding the payout until our refund policy matches the minimum in their Checkout Buyer Terms: a customer may cancel and get a refund within 14 days without giving a reason. The copy guardrails did the opposite - they forbade the 14-day wording across the pricing, support and refunds copy. Flip them to require it, and update the code-owned strings they pin. Requests past the window stay reviewed case by case, and the stale 24-hour Lifetime promise stays forbidden. The matching Sanity content (terms, privacy, refunds body, pricing FAQ) is not part of this commit and still has to be written in Studio, so sanity:verify:site will fail against published content until it lands. Co-Authored-By: Claude Opus 5 --- lib/direct/commerce-contract.ts | 2 +- lib/direct/support-content.ts | 4 ++-- scripts/verify-sanity-site.ts | 8 ++++++-- tests/direct-commerce-contract.test.ts | 4 +++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/direct/commerce-contract.ts b/lib/direct/commerce-contract.ts index 4026242..fdf6bd3 100644 --- a/lib/direct/commerce-contract.ts +++ b/lib/direct/commerce-contract.ts @@ -81,7 +81,7 @@ export const DIRECT_PRICING_FAQ_ITEMS = [ { question: "What is your refund policy?", answer: - "Direct refund requests are reviewed case by case. A refund and cancellation of automatic renewal are separate actions, so tell support which outcome you need.", + "You can cancel and get a full refund within 14 days of any Direct payment, without giving a reason. Later requests are reviewed case by case. A refund and cancellation of automatic renewal are separate actions, so tell support which outcome you need.", }, { question: "What happens after my subscription expires?", diff --git a/lib/direct/support-content.ts b/lib/direct/support-content.ts index 3baa835..eda2274 100644 --- a/lib/direct/support-content.ts +++ b/lib/direct/support-content.ts @@ -1,8 +1,8 @@ export const DIRECT_SUPPORT_REFUND_TITLE = "Direct purchase refunds"; export const DIRECT_SUPPORT_REFUND_BODY = - "Direct refund requests are reviewed case by case. A refund and cancellation of automatic renewal are separate actions."; + "You can cancel and request a full refund within 14 days of any Direct payment, without giving a reason. Later requests are reviewed case by case. A refund and cancellation of automatic renewal are separate actions."; export const DIRECT_SUPPORT_REFUND_FAQ = - "Choose “Paddle refund (direct purchase)” in the support form and use the email address from your Paddle receipt. Add the transaction ID and say whether you want a refund only or a refund plus cancellation of automatic renewal. Requests are reviewed case by case. Mac App Store purchases are refunded by Apple."; + "Choose “Paddle refund (direct purchase)” in the support form and use the email address from your Paddle receipt. Add the transaction ID and say whether you want a refund only or a refund plus cancellation of automatic renewal. Refunds requested within 14 days of a payment are granted without giving a reason; later requests are reviewed case by case. Mac App Store purchases are refunded by Apple."; export const HORA_INSTALLATION_FAQ = "You can download the Direct edition from horacal.app and choose a plan in the app, install hora from the Mac App Store, or use it through Setapp. Direct includes a 7-day cardless trial before you choose Monthly or Annual."; diff --git a/scripts/verify-sanity-site.ts b/scripts/verify-sanity-site.ts index fdee71c..0d4eb44 100644 --- a/scripts/verify-sanity-site.ts +++ b/scripts/verify-sanity-site.ts @@ -369,7 +369,8 @@ function validatePricing(document: SiteDocument) { expectCopy(publicCopy, "7-day", "pricingPage public copy"); expectCopy(publicCopy, "cardless", "pricingPage public copy"); expectCopy(publicCopy, "no new lifetime", "pricingPage public copy"); - for (const forbidden of ["14-day", "14 day", "24-hour", "24 hour"]) { + expectCopy(publicCopy, "14 days", "pricingPage public copy"); + for (const forbidden of ["24-hour", "24 hour"]) { expect(!publicCopy.includes(forbidden), `pricingPage public copy must not promise ${forbidden}`); } } @@ -604,13 +605,15 @@ function validateLegal(document: SiteDocument, kind: "privacy" | "terms" | "refu if (kind === "refunds") { const publicCopy = searchableText(document.body); + expectCopy(publicCopy, "within 14 days", "refundsPage.body"); + expectCopy(publicCopy, "without giving a reason", "refundsPage.body"); expectCopy(publicCopy, "reviewed case by case", "refundsPage.body"); expectCopy( publicCopy, "refund and cancellation of automatic renewal are separate actions", "refundsPage.body", ); - for (const forbidden of ["14-day", "14 day", "24-hour", "24 hour"]) { + for (const forbidden of ["24-hour", "24 hour"]) { expect(!publicCopy.includes(forbidden), `refundsPage.body must not promise ${forbidden}`); } } @@ -635,6 +638,7 @@ function validateCodeOwnedDirectSupport() { DIRECT_SUPPORT_REFUND_FAQ, ]); expectCopy(publicCopy, "direct purchase refunds", "support Direct refund copy"); + expectCopy(publicCopy, "within 14 days", "support Direct refund copy"); expectCopy(publicCopy, "reviewed case by case", "support Direct refund copy"); expectCopy( publicCopy, diff --git a/tests/direct-commerce-contract.test.ts b/tests/direct-commerce-contract.test.ts index c2016ed..d9e9ccb 100644 --- a/tests/direct-commerce-contract.test.ts +++ b/tests/direct-commerce-contract.test.ts @@ -45,7 +45,9 @@ test("keeps Direct new-sale pricing in code while Sanity controls download visib const publicCopy = JSON.stringify(DIRECT_PRICING_FAQ_ITEMS).toLowerCase(); assert.match(publicCopy, /7-day cardless trial/); assert.match(publicCopy, /no new lifetime plan/); - assert.doesNotMatch(publicCopy, /14-day|14 day|24-hour|24 hour/); + assert.match(publicCopy, /within 14 days of any direct payment/); + assert.match(publicCopy, /reviewed case by case/); + assert.doesNotMatch(publicCopy, /24-hour|24 hour/); assert.equal( DIRECT_CHECKOUT_PRICE_NOTE, "Choose a plan in the app. Final currency and applicable taxes are confirmed in checkout.",