Skip to content

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

AreaRoutePurposeImplementation file
Admin promo management/app/promo-codesCreate, edit, deactivate, and review promo usageapp/routes/app.promo-codes.jsx
Admin promo sending/app/promo-sendSend promo by email, review send activity, resendapp/routes/app.promo-send.jsx
Merchant promo redemption/app/promo-redeemRedeem promo and increase free-plan reply allowanceapp/routes/app.promo-redeem.jsx

Billing entry point:

  • app/routes/app.billing.jsx includes a “Have a promo code?” card with a link to /app/promo-redeem.

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() and assertPromoAdmin() in app/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.


Prisma models are defined in prisma/schema.prisma.

FieldDescription
titleRequired promo title for admin readability
codeUnique normalized code
freeRepliesNumber of free replies granted on successful claim
descriptionOptional admin note/description
claimModeonce or unlimited
activeControls redeem eligibility

One row per successful redemption.

FieldDescription
promoCodeIdFK to promo code
shopId / shopDomainClaimed shop reference
claimedByName / claimedByEmailClaimer identity snapshot
onceLockKeyDB-level lock for single-use promos (claimMode = once)
claimedAtClaim timestamp

One row per (promoCodeId, recipientEmail) pair.

FieldDescription
promoCodeIdFK to promo code
recipientEmail / recipientNameTarget recipient
customNoteOptional send note
sentByEmailAdmin sender identity
sentAtLast sent timestamp
sendStatus / sendErrorDelivery status metadata

Constraint:

  • Unique index on (promoCodeId, recipientEmail) prevents duplicate rows for the same promo+recipient combination.

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.

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_limit by promo freeReplies
    • do not increment shops.free_credits_used

Net effect:

  • Promo claims increase free-plan monthly allowance headroom.
  • Existing “used replies” counter remains unchanged at claim time.

Send workflow is implemented in app/routes/app.promo-send.jsx.

  • Validates active promo and recipient email.
  • Sends invite email.
  • Writes/updates PromoCodeSendLog.
  • Uses the existing promo send-log row.
  • Sends invite email again from existing row context.
  • Updates sentAt, sentByEmail, sendStatus, and sendError.
  • Does not insert a new row.

This keeps one clear audit row per promo+recipient and avoids repeated duplicate records.


Promo-related helpers in app/lib/email.server.js:

  • sendPromoClaimConfirmationEmail (merchant confirmation after claim)
  • sendPromoClaimInternalNotificationEmail (internal alert to info@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.

Promo functionality is introduced by:

  • 20260424160000_add_promo_code_module
  • 20260424171000_add_promo_title
  • 20260424183000_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.

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 PromoCodeSendLog row is updated (sentAt changes) instead of inserting a second row.