Orders & fulfilment
Credit Memo Documents
Generated from docs/plan/core/credit-memo-documents/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Generates and stores formal credit memo documents tied to refunds, enabling merchants to provide bookkeepers with audit-trail artifacts for accounting reconciliation. Merchants can download PDFs from the order detail view.
The problem
When I issue a refund, my bookkeeper has no formal document to reconcile the transaction. Refund status exists in the system but there’s no audit trail, no memo number, no formatted output for accounting.
What it does
- Create Refund entity with refund_id, amount, reason, status, created_at fields
- Model credit memo as a document with memo number (auto-sequence per year), refund reference, line items, totals
- Generate PDF credit memos (plain HTML-to-PDF, no branding; merchants can re-print)
- Store memo as audit log entry + blob/file entity for retrieval
- Admin UI: list refunds, view credit memo, download as PDF, reprint memo
- REST endpoint GET /orders/:id/refunds and GET /orders/:id/refunds/:refund_id/memo (PDF)
- Webhook event: order.refund_issued with memo URL
- Settings: credit memo number prefix, footer text, issuing merchant name
- Negative case: refund created but memo generation fails—memo is not retried silently; staff must see failure
- Multi-currency: memo shows currency code and amount in order currency, not system currency
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Partial refunds: out of scope—refund model supports only full refunds initially, because partial logic couples to line-item allocation (deferred to invoice feature). Reason: schema churn; we defer partial refunds to shipment integration when line-level tracking lands.
- Automated refund rules: out of scope—no rule engine for auto-refunding on return receipt or failed payment. Reason: adds async state machine; merchants start with manual refunds + webhooks to ERP.
- Refund reversal (credit memo reversal): out of scope—if a memo is wrong, staff deletes the refund and creates a new one. Reason: reduces schema depth; reversal is rare and handled by audit log review.
- Tax calculation on refunds: out of scope—memo uses refund amount as-is; tax recalculation is merchant’s bookkeeper duty. Reason: tax engine is not in scope; merchants responsible for tax compliance.
- Multi-language memo templates: out of scope—memo is English only. Reason: localization infrastructure not in scope; merchants customize via footer text setting.
- Electronic filing (e.g., to revenue agency): out of scope. Reason: jurisdiction-specific; merchant’s accountant handles.
Data model
Migration: new Refund table/collection (refund_id, order_id, amount_minor, reason, status=‘completed’, created_at, memo_id). New Memo table/collection (memo_id, refund_id, memo_number, year, PDF_blob_id, created_at). Assumption: audit_log already exists.
API
- POST /orders/:id/refunds (body: {amount_minor, reason}) → {refund_id, memo_id, status}
- GET /orders/:id/refunds → [{refund_id, amount, reason, memo_id, created_at, status}]
- GET /orders/:id/refunds/:refund_id → {refund_id, amount, reason, memo_id, status, created_at}
- GET /orders/:id/refunds/:refund_id/memo (returns PDF blob)
- DELETE /orders/:id/refunds/:refund_id (cancel refund, delete memo) — staff only
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Order detail view shows Refunds tab. Staff sees list of refunds with memo number, amount, reason, date. Click refund to see details and download PDF. Button to issue refund (opens modal for amount + reason). Refund can be cancelled before memo is finalised, but not after.
The seam — why this is core
Core owns refund entity, memo generation, and the REST interface. Paid pack would own: multi-currency tax recalculation, partial refunds, automated refund rules. Why: core provides the honest hand-modelled refund (full refund + audit trail); tax and partial refunds require jurisdiction knowledge (paid) and line-item schema (deferred).
Accounting seam. Refunds are modelled correctly (running total bounds further refunds), but the document artefact is missing. This is part of the order interface seam.
Dependencies
- orders (order entity must exist with currency, amount fields)
- audit-log (refund create/delete events logged)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- A refund of 10000 (100.00 USD) generates a memo number, stores it, and returns memo_id in response
- Memo PDF contains order number, refund amount, currency, memo number, date, and merchant name from settings
- Memo number increments per year: refunds in 2026 start at 001, refunds in 2027 start at 001
- GET /orders/123/refunds/refund_456/memo returns Content-Type: application/pdf, not JSON
- Deleting a refund deletes the memo; memo file is no longer retrievable
- Refund with reason ‘customer request’ stores that reason and displays it in admin
- Refund issued webhook fires with refund_id and memo URL; webhook retry works if first delivery fails
- Multi-currency order (EUR 100.00) refunded shows EUR and 10000 in memo, not system currency
- No refund can exceed order total; POST returns 400 and order.refund_exceeded error
- Memo generation failure (e.g., PDF encoder error) does not silently retry; admin sees ‘memo generation failed’ status
Risks
Schema: refund is a new first-class entity—migrations on three driver types (JSON, libSQL, SQL) must all succeed or refunds are broken on one tier. Accounting: a refund without a memo is a black hole for bookkeepers; if memo generation fails, merchant gets no alert and memo is never retried. Audit: if refund is deleted but memo blob remains, bookkeeper has orphaned documents.
Commercial context
| Suggested price | Free |
| Rival anchor | Magento ships credit-memo entities and generation free as part of the refund flow. |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.