Promo code functionality
This document describes the implemented promo code feature in AppiFire AI Chat, including:
- Admin promo management (create, edit, deactivate)
- Merchant promo redemption from Billing
- Promo send/resend email workflows
- Database constraints and migration notes
1. Routes and entry points
Section titled “1. Routes and entry points”| Area | Route | Purpose | Implementation file |
|---|---|---|---|
| Admin promo management | /app/promo-codes | Create, edit, deactivate, and review promo usage | app/routes/app.promo-codes.jsx |
| Admin promo sending | /app/promo-send | Send promo by email, review send activity, resend | app/routes/app.promo-send.jsx |
| Merchant promo redemption | /app/promo-redeem | Redeem promo and increase free-plan reply allowance | app/routes/app.promo-redeem.jsx |
Billing entry point:
app/routes/app.billing.jsxincludes a “Have a promo code?” card with a link to/app/promo-redeem.
2. Access control (promo admin)
Section titled “2. Access control (promo admin)”Promo admin access is restricted to zainalam1001@gmail.com.
Implementation behavior:
- Navigation visibility is controlled in
app/routes/app.jsx(promo links only shown when access passes). - Server-side authorization is enforced via
canAccessPromoAdmin()andassertPromoAdmin()inapp/lib/promo.server.js. - Access passes if either:
- resolved admin/session email matches the allowlist email, or
- stored shop email matches the allowlist email.
Route/action guards are enforced server-side, not only via hidden nav links.
3. Database model
Section titled “3. Database model”Prisma models are defined in prisma/schema.prisma.
3.1 PromoCode
Section titled “3.1 PromoCode”| Field | Description |
|---|---|
title | Required promo title for admin readability |
code | Unique normalized code |
freeReplies | Number of free replies granted on successful claim |
description | Optional admin note/description |
claimMode | once or unlimited |
active | Controls redeem eligibility |
3.2 PromoRedemption
Section titled “3.2 PromoRedemption”One row per successful redemption.
| Field | Description |
|---|---|
promoCodeId | FK to promo code |
shopId / shopDomain | Claimed shop reference |
claimedByName / claimedByEmail | Claimer identity snapshot |
onceLockKey | DB-level lock for single-use promos (claimMode = once) |
claimedAt | Claim timestamp |
3.3 PromoCodeSendLog
Section titled “3.3 PromoCodeSendLog”One row per (promoCodeId, recipientEmail) pair.
| Field | Description |
|---|---|
promoCodeId | FK to promo code |
recipientEmail / recipientName | Target recipient |
customNote | Optional send note |
sentByEmail | Admin sender identity |
sentAt | Last sent timestamp |
sendStatus / sendError | Delivery status metadata |
Constraint:
- Unique index on
(promoCodeId, recipientEmail)prevents duplicate rows for the same promo+recipient combination.
4. Promo code format and normalization
Section titled “4. Promo code format and normalization”Shared helpers in app/lib/promo.shared.js normalize and format promo values.
- Input accepts case-insensitive values.
- Stored values are normalized uppercase alphanumeric.
- Hyphens are removed for storage and applied for display formatting.
- Validation enforces supported characters and normalized length limits.
5. Redemption rules and quota behavior
Section titled “5. Redemption rules and quota behavior”Redeem logic is implemented in app/routes/app.promo-redeem.jsx.
Behavior:
- Redemption is allowed only when
shop.plan === "free". - Inactive or invalid promo codes are rejected.
- Claim-mode enforcement:
once: first successful claim only (global single-use)unlimited: multiple successful claims allowed
- On successful redemption (transactional flow):
- create
PromoRedemption - increment
shops.free_credit_limitby promofreeReplies - do not increment
shops.free_credits_used
- create
Net effect:
- Promo claims increase free-plan monthly allowance headroom.
- Existing “used replies” counter remains unchanged at claim time.
6. Admin send and resend flow
Section titled “6. Admin send and resend flow”Send workflow is implemented in app/routes/app.promo-send.jsx.
6.1 Send
Section titled “6.1 Send”- Validates active promo and recipient email.
- Sends invite email.
- Writes/updates
PromoCodeSendLog.
6.2 Resend
Section titled “6.2 Resend”- Uses the existing promo send-log row.
- Sends invite email again from existing row context.
- Updates
sentAt,sentByEmail,sendStatus, andsendError. - Does not insert a new row.
This keeps one clear audit row per promo+recipient and avoids repeated duplicate records.
7. Promo email templates
Section titled “7. Promo email templates”Promo-related helpers in app/lib/email.server.js:
sendPromoClaimConfirmationEmail(merchant confirmation after claim)sendPromoClaimInternalNotificationEmail(internal alert toinfo@appifire.com)sendPromoCodeInviteEmail(admin send/resend invite)
Template characteristics:
- Plain-text body + branded HTML body.
- Styled to align with AppiFire email look and feel.
- Dynamic values are escaped before HTML interpolation.
8. Migrations
Section titled “8. Migrations”Promo functionality is introduced by:
20260424160000_add_promo_code_module20260424171000_add_promo_title20260424183000_unique_promo_send_per_recipient
Migration note:
- The unique-send migration includes deduplication logic before applying the unique index to avoid failures on existing duplicate data.
9. Operational checks
Section titled “9. Operational checks”If promo links are missing:
- Verify resolved admin/session or shop email is
zainalam1001@gmail.com. - Reload embedded app session after authentication.
If migration reports failed state (P3009):
- Resolve the failed migration state first, then run deploy again.
If resend behavior is questioned:
- Confirm the same
PromoCodeSendLogrow is updated (sentAtchanges) instead of inserting a second row.