mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
The billing cycle takes one step per live subscription (engine.Settle) and reports every one: org, plan, interval, paid period, billed period, price, amount, source and action. Without dryRun=false it writes nothing. - A renewal bills the period after the paid one from sub.Plan, the plan as bought, on the source it was bought from: its card, or prepaid money (credits, then the balance) for a plan bought with credits or the balance. A plan collected outside Hanzo is renewed by its processor and never charged. - A decline is retried 1, 3 and 7 days after it; the subscriber keeps the tier meanwhile, and the last decline ends the subscription. - A subscription more than 72 hours past its paid period with no attempt is overdue: nothing is charged or written until its customer renews it, which bills the period from then and never the time it was not renewed. - Realignment is an operator's act with its own dry run (POST /v1/billing/realign/run[-all]): a row whose PeriodStart is its current paid invoice's PeriodEnd moves back onto that invoice's period. - Every payment on an invoice holds one lock and records its attempt before asking for money; an unknown outcome is repeated under its key, escalated to unpaid after the schedule, and resolved before any cancel or end voids it. - A cancel landing while a renewal is at the processor stays canceled; a charge that does not answer in 30 seconds is unknown; one org's cycle stops starting subscriptions after 10 minutes. - A voided or uncollectible invoice returns what it collected to prepaid money; tax is set on drafts only; a platform operator voids an unresolved attempt through POST /v1/billing/invoices/:id/void-unresolved with a reason recorded in the billing event ledger. - How a subscription was bought is read from its earliest invoice that bills a period. Co-authored-by: Hanzo Dev <[email protected]>
34 lines
1.1 KiB
Go
34 lines
1.1 KiB
Go
package billing
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/commerce/datastore"
|
|
"github.com/hanzoai/commerce/util/test/ae"
|
|
)
|
|
|
|
// TestPayInvoice_KeepsTheInvoiceListed: paying an invoice writes it back, and
|
|
// it stays in the invoice list. The list reads invoices under no ancestor, so a
|
|
// row read by id and written back is listed like any other.
|
|
func TestPayInvoice_KeepsTheInvoiceListed(t *testing.T) {
|
|
ctx := ae.NewContext()
|
|
defer ctx.Close()
|
|
org := moneyOrg("inv-listed")
|
|
withFakeSquare(t, squareMock("", "", "sqpay_listed"))
|
|
db := datastore.New(org.Namespaced(ctx))
|
|
sub := seedCardBackedSub(t, db, "inv-listed", "dev", "ccof_il", "cust_il")
|
|
inv := seedOpenInvoice(t, db, sub, 2000)
|
|
|
|
if resp := invokePay(org, ctx, inv.Id()); resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("pay status=%d, want 200", resp.StatusCode)
|
|
}
|
|
listed, err := ListInvoices(ctx, org, "", "", sub.Id(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if len(listed) != 1 || listed[0].ID != inv.Id() || listed[0].Status != "paid" {
|
|
t.Fatalf("invoice list after paying = %+v, want the one invoice, paid", listed)
|
|
}
|
|
}
|