Skip to content
Club system guide

Receipts and membership

How the club reads TooCOOL receipts and prevents the same order from being processed twice.

A purchase receipt arrives by email, not through a trusted payment webhook. The system checks the sender and recipient, reads the email and its PDF, and makes sure the same purchase is not processed twice.

Two services share the work. The Email Worker reads the message. The API processes the order. Both save progress so a retry does not repeat finished work.

System flow

How a receipt becomes a purchase

The Email Worker reads receipts and retries temporary failures. D1 records the result. Stable provider IDs make repeated calls safe.

Read and claim the receipt

  1. Email Routing Delivers a purchase receipt
  2. Email checks Checks the recipient, sender, and raw message size
  3. postal-mime Reads the message and its attachments
  4. unpdf Reads text from the receipt PDF
  5. TooCOOL parser Finds the order and each item
  6. KV lease Stops duplicate receipt processing
  7. D1 claim Claims the fulfillment idempotency key

Complete a membership purchase

  1. Membership Creates an activation key
  2. Resend Uses a stable Idempotency-Key
  3. Discord event Uses a predictable message nonce

Complete another purchase

  1. Film or print Finds the purchased item type
  2. Discord event Notifies the club staff

Handle a failure

  1. Temporary failure Saves a retry record in KV
  2. Five-minute Cron Tries to process the receipt again
  3. Final failure Saves the normalized result and stops retrying
KV prevents duplicate intake work. D1 owns the purchase result. Stable provider IDs prevent a lost response from causing a duplicate message or email.

Cloudflare Email Routing forwards messages for the configured purchase address to the Email Worker. The Worker receives the envelope and raw message stream through its email handler.

Before reading the body, it checks:

  • The recipient matches the configured receipt address.
  • The sender is allowed by the forwarding policy.
  • The receipt intake rate limit has not been reached.

The Worker enforces the raw-message byte limit as it reads the stream. After it parses the email’s MIME structure, it checks the attachment count, PDF size, and whether the message contains a supported purchase. MIME is the email format that separates headers, text, HTML, and attachments. The Worker checks its API and KV bindings later, when the parsed purchase enters the processing flow.

These early checks stop the Worker from spending time on a message it cannot use.

The Worker reads the message in three steps.

postal-mime splits the raw email into headers, text, HTML, and attachments. It handles MIME boundaries, text encodings, and filenames so the purchase parser does not have to.

TooCOOL receipts may store the order details in a PDF. unpdf extracts the text so the Worker does not treat raw PDF bytes as text.

The code uses unpdf, not pdfjs-dist. Check the right package when changing the parser or debugging the production bundle.

The TooCOOL parser turns the receipt text into an order ID, purchaser details, line items, quantities, and known membership, film, or print products.

Parsing does not create a membership or send a notification. It only prepares a limited request for the API.

The API checks the parsed order again before it creates a key, sends an email, or updates Discord.

The same email can arrive more than once. A provider may retry after a timeout, a forwarding rule may send it again, or the Worker may lose a response after finishing part of the work.

After MIME, PDF, and TooCOOL parsing, the RECEIPT_DEDUPE KV namespace tracks each normalized purchase. A normalized purchase is the clean, standard order data made by the parser. Its stable purchase key lets the Worker:

  • Claim a processing lease. A lease is a short lock that stops another delivery from doing the same work at the same time.
  • Find an intake that is already complete.
  • Save a temporary failure and the next time it may retry.
  • Save a permanent failure so the same normalized purchase does not loop forever.
  • Keep the state for a limited TTL (time to live) instead of forever.

KV protects submission of the normalized purchase and retries to the API. It does not deduplicate the raw email or prove that the API finished the order.

Second duplicate check: D1 fulfillment claim

Section titled “Second duplicate check: D1 fulfillment claim”

The API receives the parsed receipt through a private service-binding route. Before it sends an email, creates an activation key, or starts Discord work, it claims an idempotency record in D1’s receipt_fulfillment table. Idempotency means a retry can use the same request without creating the result twice.

This D1 record sits next to the order result, so it is the final duplicate check. Two emails can produce the same parsed purchase, but only one request can own the fulfillment.

KV check

Was this parsed purchase already sent?

Stops repeated API submissions, schedules retries, and tracks parsed purchases.

D1 check

Was this order already processed?

Stops duplicate activation keys, emails, membership changes, and Discord notifications.

D1 owns fulfillment, but an outside service can finish a call just before the API saves that step. The code uses stable IDs to make retries safe:

  • Resend receives an Idempotency-Key derived from the D1 fulfillment row.
  • The Discord event receives a fixed nonce derived from the same fulfillment record.

If the provider accepts a call but the next D1 marker write is lost, the retry uses the same ID. It does not create a second email or Discord message.

For a known membership product, the API:

  1. Claims the receipt fulfillment in D1.
  2. Matches the product to a membership tier and duration.
  3. Creates an activation key with those membership terms.
  4. Sends the key through Resend with the fulfillment’s stable idempotency key.
  5. Sends a signed Discord event with the fulfillment’s fixed nonce.
  6. Saves the result so the same purchase cannot repeat those actions.

The purchaser redeems the key through the normal account flow. The API still controls redemption, membership, expiry, and Discord role updates. The email parser cannot change them by itself.

The sender uses the club’s membership address. The Resend API key stays in the Worker secret settings. It is not in the repository or website.

Film and print purchases do not create a membership key. The parser identifies those items, the API claims the fulfillment, and the API sends the right Discord notification to club staff.

The duplicate checks still apply. A receipt sent twice must not create two “new film order” messages.

Some failures can be retried. Others should stop.

Failure typeExampleWorker behavior
TemporaryAPI binding timeout, provider failure, or short storage outageSave a KV retry record and try again after the configured delay.
Email rejectionWrong recipient, unsupported sender, missing PDF, or no supported purchaseReject the Email Routing message. These paths do not create a final KV record.
Permanent parsed-purchase failureA parsed purchase cannot be processed after it enters the flowSave the final KV result and stop automatic retries.
DuplicateThe parsed purchase or D1 fulfillment is already completeReturn the saved result without repeating emails, keys, or notifications.
In progressAnother delivery holds the active leaseDo not start a second parse or fulfillment.

The Email Worker has a scheduled job, or cron, that checks for eligible KV retries every five minutes. It is separate from the platform Scheduler Worker, which runs club jobs every thirty minutes. The Email Worker owns receipt retry timing.

A service can finish the work even if the caller never receives its response.

For example, the API may create an activation key and Resend may accept the email, but the next email_sent_at write or the final response may be lost. A retry returns to the same D1 fulfillment and activation key. It calls Resend with the same idempotency key, so Resend can return the first send instead of delivering another email. A Discord retry uses the same fixed nonce to avoid a second message.

KV only tracks the delivery attempt. D1 tracks the saved membership result. Stable provider IDs cover the gap between an outside service finishing its work and D1 saving that step.

The fulfillment route is private. It requires the Email Worker’s identity and internal token through the service binding. A public browser cannot submit a fake receipt to get a membership key.

The API checks the parsed fields again. It uses parameterized database operations, keeps secrets out of error responses, and signs each Discord event before sending it.

Live Email Routing rules and production sender settings are stored outside the repository. The code defines the expected address, binding, parsing flow, and checks. This page does not confirm that the live mailbox rule was checked when it was written.

Expanded photograph