Skip to content

docs: add design document for customer-portal receipts#11188

Open
psincraian wants to merge 1 commit intomainfrom
psincraian/silly-liskov-a180f4
Open

docs: add design document for customer-portal receipts#11188
psincraian wants to merge 1 commit intomainfrom
psincraian/silly-liskov-a180f4

Conversation

@psincraian
Copy link
Copy Markdown
Contributor

Summary

  • Adds a design document at handbook/engineering/design-documents/receipts.mdx for the upcoming customer-portal Receipts feature.
  • Captures the problem (the existing invoice flow is gated on billing details, leaving paid customers with no downloadable proof of payment), the proposed solution (one mutable PDF per Order with a succeeded Payment), and the rollout plan (per-org boolean flag plus a backfill script).
  • Documents the key design trade-offs: per-Order receipt with stable number, lazy render in a Dramatiq actor with a Redis-locked single-renderer, immutable per-render S3 keys in a new dedicated bucket, hardcoded RCPT- number prefix.

No code changes — documentation only.

Test plan

  • Open the rendered handbook locally and verify the new doc renders correctly.
  • Review the design with the team.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@psincraian psincraian requested a review from a team as a code owner April 23, 2026 11:19
@mintlify
Copy link
Copy Markdown

mintlify Bot commented Apr 23, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Handbook 🟢 Ready View Preview Apr 23, 2026, 11:19 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
polar Ready Ready Preview, Comment Apr 23, 2026 11:21am
polar-sandbox Ready Ready Preview, Comment Apr 23, 2026 11:21am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

Preview Environment
URL: https://polar-preview-vm.taildbff7b.ts.net/pr-11188
API: https://polar-preview-vm.taildbff7b.ts.net/pr-11188/v1/
Logs: backend
SHA: 8741ef8220fce08c7d276c357966269ea34c25d9


## Briefing

The customer portal already exposes "Download Invoice" on every paid Order, but invoice generation requires `billing_name` and `billing_address`. Customers who never fill those in (indie devs, wallet payers, anyone topping up balance credit) get nothing. Support sees regular requests for proof of payment, and some customers actually require it for bookkeeping, expense reimbursement, or compliance. They don't need a tax invoice, just confirmation that the charge happened.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customers who never fill those in (indie devs, wallet payers, anyone topping up balance credit) get nothing.

That's not true :) Invoices are generated automatically and attached to the confirmation email. It shows the billing info we have (so the customer name and country at the bare minimum).

I think the heart of the problem is people who want a proof of payment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's my fault and wrong assumption then. Will fix it


The customer portal already exposes "Download Invoice" on every paid Order, but invoice generation requires `billing_name` and `billing_address`. Customers who never fill those in (indie devs, wallet payers, anyone topping up balance credit) get nothing. Support sees regular requests for proof of payment, and some customers actually require it for bookkeeping, expense reimbursement, or compliance. They don't need a tax invoice, just confirmation that the charge happened.

A **receipt** is the missing artifact: a PDF proving the payment event without the billing-details gate. It mirrors Stripe's `charge.receipt_url`. The PDF is mutable, so refunds are reflected in the same document over time.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't how it should work.

Receipts should match bank transactions (for the end customer).

So if you as the buyer have on April 23 a charge of $29.94 and on May 7 a refund incoming of $29.94, you will need two receipts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not right :)

Stripe charge.receipt.url is a mutable document that's keep up to date with the latest payment information.

With François, we discovered that when a refund is made the url gets up to date. For the refund one, the original payment is shown and the refunded amount.

Receipt-2224-6958.pdf
Refund-3962-0567.pdf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's still two different documents, right? Surely the receipt.url reference is updated, but I'm pretty sure both still exist in Stripe's records somewhere somehow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the S3 we are gonna store all documents just in case.

The idea is to store them in {orgId}/{orderIt}/{timestamp}.pdf


### Refund regeneration

When a refund is finalized, the refund hook calls into the receipt service to enqueue a render. The old `receipt_path` keeps serving until the new file is uploaded and the column is swapped. There is no null gap: customers downloading mid-window get the previous PDF, and the next download gets the new one with the Refunds section.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered above


None outstanding for v0. The following are explicitly deferred:

- Receipts for Orders without a succeeded Payment. Could be revisited if support volume justifies it.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is not a thing? What would the receipt be for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this would be needed, the receipt will mainly show: "Discount applied. Free order" or similar. I don't think this is needed that's why I deferred it

None outstanding for v0. The following are explicitly deferred:

- Receipts for Orders without a succeeded Payment. Could be revisited if support volume justifies it.
- Email delivery.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was Isac who raised a good point here: if we generate receipts but we don't give merchants the option to send them automatically, customers will keep asking for it over support instead of actually fixing the problem we're trying to self-serve. So it's worth considering if we should email them, and if so, when and how. In other words, can they be bundled in with an existing email? Theoretically, that should be possible for one-time purchases and subscription starts (as the payment proceeds), for subscription renewals, it's harder.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the beginning, I was thinking to add only a to the order view in the customer portal. So the user can click it if they need to download it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants