Rebounder Tech Blog

Written by the people who actually run these systems in production.

share_links.kind Lacked 'receipt': a Feature Vanished

Published About 6 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

A feature can be fully built on its own branch, but once that branch never merges and trunk drifts 82 commits ahead, every trunk deploy quietly regresses it out of existence.

A feature can be fully built and working on its own branch, but once that branch has never merged and trunk has drifted 82 commits ahead of it, every deploy from trunk quietly regresses the feature out of existence.

Conclusion

The receipt feature was implemented and working on a separate branch, feat/receipt-ryoshusho, as of 2026-06-13 — but it was never once merged into the current trunk. Trunk kept moving forward in the meantime, and by the time this was fixed it had advanced 82 commits past that branch. A trunk deploy can obviously only ship code that exists in trunk, so every single deploy left the receipt feature completely absent. The fix wasn’t to merge the stale branch — it was to reimplement the feature as a sibling of trunk’s current code.

Symptom

  • The receipt feature was complete on its branch, but there was no trace of it ever having been merged into trunk.
  • Trunk had re-written the CHECK constraint on the share_links.kind column — which tags what kind of share link a row represents — multiple times (migrations 0040/0044/0045/0054), each time for an unrelated reason such as consolidating invoices.
  • Each rewrite rebuilt the constraint from “the set of kinds trunk already knows about,” so the 'receipt' value the branch had added existed, from trunk’s perspective, nowhere at all.
  • Since a deploy can only reflect trunk’s code and trunk’s DB schema, the branch’s feature ended up “fully implemented, yet absent the moment it’s deployed.”
  • It never surfaced as a bug report or an error log entry — it surfaced as the feature simply not being there at all (or, later, as issuing a receipt failing a CHECK-constraint violation).

Cause

Trunk’s most recent rewrite of that CHECK constraint was migration 0054, which added support for group_invoice (consolidated billing for groups of contracts).

-- 0054_share_links_group_invoice.sql (a1eb39d272, lines 10-15)
-- Allow 'group_invoice' as a kind (rebuilt while keeping every kind known through 0045).
alter table public.share_links
  drop constraint if exists share_links_kind_check;
alter table public.share_links
  add constraint share_links_kind_check
  check (kind in ('invoice','quote','application','report','file','bundle','group_invoice'));

As its own comment says, this constraint faithfully piles up only the kinds trunk already knows about — which is the correct thing to do in isolation. The problem is that whoever wrote migration 0054 (2026-06-21, eight days after the branch started) had no way of knowing that the feat/receipt-ryoshusho branch had added 'receipt' back in migration 0031 on 2026-06-13. The branch had never been merged, so as far as anyone on trunk was concerned, a kind called 'receipt' simply didn’t exist.

How far the two had drifted apart by the time this was fixed is easy to confirm by commit count.

$ git log --oneline feat/receipt-ryoshusho..<commit right before the fix on trunk> | wc -l
82

The fix

Rather than merging a branch that was 82 commits stale, the fix reimplemented the receipt logic from scratch as a sibling of trunk’s current invoice-issuing code (invoice-issue.ts).

// receipt-issue.ts (3f1d797b2b, opening lines of 1-101)
/**
 * Issues a contract's receipt and emails it to the customer (sibling of invoiceIssueAndEmail).
 * - Reuses an existing active receipt share link if one exists. The link is /p/[token] (no login required).
 * - Called only on the transition of payment status to "paid" (admin/contracts' updatePaymentStatus,
 *   or finance's reconcilePayment). Callers guard with prevPayment so re-saving or re-reconciling doesn't resend.
 * - Contracts billed to a group (billed_to other than self) are handled by consolidated billing
 *   (group_invoice) instead — mirrors invoiceIssueAndEmail (0053/0054).
 */
export async function issueReceiptAndEmail(
  supabase: DbClient,
  contractId: string,
  issuerId: string | null
): Promise<boolean> {

Next, the CHECK constraint was rebuilt idempotently as the union of every kind known through 0054 plus 'receipt'.

-- 0055_share_links_receipt_restore.sql (3f1d797b2b, lines 1-19)
-- Background: the receipt feature (feat/receipt-ryoshusho, originally migration 0031,
--   shipped 2026-06-13) added 'receipt' to share_links.kind. But trunk's 0040/0044/0045/0054
--   each rewrote the kind CHECK "without receipt," so on current trunk — i.e. production —
--   'receipt' is no longer allowed, and issuing a receipt (creating a share_link with kind='receipt')
--   fails the CHECK constraint.

alter table public.share_links drop constraint if exists share_links_kind_check;
alter table public.share_links
  add constraint share_links_kind_check
  check (kind in ('invoice','quote','application','report','file','bundle','group_invoice','receipt'));

Finally, to make sure automatic receipt issuance fires only at the exact moment a contract’s payment status transitions to “paid,” both call sites — the contract-status update action and the finance reconciliation action — were changed to fetch the payment status before the update and compare it, so re-saving or re-reconciling the same already-paid state never sends a receipt twice.

// admin/contracts/actions.ts (3f1d797b2b, lines 97-146)
// Fetch payment status before the update, to guard against double-issuing
// (re-saving the same status must not resend a document).
const { data: before } = await supabase
  .from("contracts")
  .select("payment_status")
  .eq("id", id)
  .maybeSingle();
const prevPayment = before?.payment_status as string | null | undefined;
// ...
// Only on the transition to "paid": notify the team immediately and auto-issue + email the receipt.
if (payment === "paid" && prevPayment !== "paid") {
  // ...
  const sentReceipt = await issueReceiptAndEmail(supabase, id, staff.id);
}
// admin/finance/actions.ts (3f1d797b2b, lines 256-292)
// Fetch payment status before the update, to guard against double-issuing
// (re-reconciling an already-paid contract must not resend the receipt).
const { data: before } = await supabase
  .from("contracts")
  .select("payment_status")
  .eq("id", contractId)
  .maybeSingle();
const prevPayment = before?.payment_status as string | null | undefined;
// ...
if (prevPayment !== "paid") {
  const sent = await issueReceiptAndEmail(supabase, contractId, user.id);
}

This same guard wasn’t added only for receipts — it was reused to close a latent double-send risk on the invoice side too (the “invoiced” transition in updatePaymentStatus).

Preventing a repeat

Nothing in this article’s sources points to any mechanism — a CI check on how far a branch has diverged, for instance — that would automatically catch a branch that’s gone unmerged for a long time. The fix here didn’t revive the branch; it settled the matter by reimplementing the feature on trunk.

Structurally, the real issue isn’t long-lived branches by themselves — it’s that a schema change like a CHECK constraint can only be validated from trunk’s point of view. Migrations 0040/0044/0045/0054 were each correct changes on their own and would pass any review. But review on trunk has no way to notice that “the set of kinds trunk knows about” and “the set of kinds production actually needs” had silently drifted apart because of an unmerged branch sitting off to the side. If a team is going to implement a feature on a long-lived branch ahead of trunk, this kind of “fully implemented, yet absent the moment it’s deployed” regression can recur unless there’s either a deadline for merging it back in, or some separate way to check whether a breaking schema change on trunk (rewriting an existing constraint, for example) collides with an unmerged feature.

Frequently asked questions

Q1Was there a bug in the receipt feature itself?

No. The receipt feature was implemented and working on a separate branch (feat/receipt-ryoshusho) from 2026-06-13. The branch was simply never merged into trunk, which advanced 82 commits past it, so every trunk deploy left the feature entirely missing.

Q2Why did this show up specifically as the feature disappearing on every deploy?

The branch had added 'receipt' to the share_links.kind CHECK constraint (migration 0031). Trunk, unaware of the branch, rewrote that constraint 3+ times for unrelated reasons (0040/0044/0045/0054), each time from only the kinds trunk itself knew — so 'receipt' never reached production.

Q3Was the fix to just merge the old branch in?

No — the 82-commits-stale branch wasn't merged. Logic was reimplemented as a sibling of trunk's invoice code (receipt-issue.ts, ReceiptDoc.tsx), and migration 0055 restored 'receipt' to the CHECK constraint. Both callers now compare payment status before/after, issuing only on the 'paid' transition.

Environment verified

  • kimiteras-portal: next 16.2.7 / @supabase/supabase-js ^2.106.2
  • Original implementation (separate branch feat/receipt-ryoshusho): 2026-06-13, 'receipt' added in migration 0031
  • Unrelated trunk-side CHECK constraint rewrites: migrations 0040/0044/0045, most recently 0054 (2026-06-21)
  • Trunk-side restoration commit: 2026-06-23 (3f1d797b2b, at the point the branch had diverged by 82 commits)

What this article is based on

  • SQL file lines 10-15commit a1eb39d
  • TypeScript file lines 1-101commit 3f1d797
  • SQL file lines 1-19commit 3f1d797
  • TypeScript file lines 97-146commit 3f1d797
  • TypeScript file lines 256-292commit 3f1d797

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.