Skip to main content

Shipping Payment

When a user buys a physical product, the checkout records two separate entries in the payments table: one for the product, one for the carrier cost. The carrier cost entry has no associated UserPlan.

If you look at the payments table and see rows where user_plan_id is null, this is why. It is not a bug; it is the carrier fee for a physical order, recorded as its own financial event.


Why a separate row

The payment ledger model requires that every financial event has its own row. The carrier cost is a separate financial event from the product purchase: it goes to a different recipient, it can be refunded independently, and it follows its own shipment lifecycle.

Storing it in the same row as the product payment would mix two separate obligations into one record. Keeping it separate means the ledger stays clean and refunds can be calculated correctly for each part.


How to identify a shipping payment row

A shipping payment row has:

  • user_plan_id = null
  • plan_type = 'shipping'
  • sale_type = 'shipping'

It shares the same order_id and gateway transaction identifier as the product payment from the same purchase. The carrier cost is never sold standalone; it always accompanies a product payment.

plan_type = 'shipping' is not a value from the Plan model. It is a synthetic value assigned by the checkout process to identify carrier-cost rows that have no plan backing them.


Recurring subscriptions

For physical subscriptions, every billing cycle produces both a product payment and a carrier cost payment. The carrier cost payment needs to reference the same billing cycle number as the product payment so that balance calculations stay correct across cycles.

Since the carrier cost row has no UserPlan, it cannot count cycles the same way as the product row. It resolves its cycle number by finding the companion product payment from the same transaction and copying the value.


Refunds on physical orders

When processing a refund on a physical order, the refund service calculates available balance separately for the product and the carrier cost. In both cases, the scope query is anchored on order_id and recurring_cycle; what differs is the additional column:

  • For the product payment: also scoped by user_plan_id
  • For the carrier cost payment: user_plan_id is null, so it is also scoped by plan_type = 'shipping'

This branching exists because the carrier cost row has no UserPlan to scope against. Without it, a refund would either miss the carrier cost entirely or calculate the wrong available amount.


Shipping notes

Once the carrier cost payment is approved, a shipping note is created for the order. The shipping note tracks the physical shipment through its lifecycle (generated, in transit, delivered, and so on).

For annual subscriptions, 12 shipping notes are created over 12 months (one per cycle). The first is created when the payment is approved. The remaining 11 are created monthly by CreateShippingNotesForSubscriptionCycles, scheduled dailyAt('02:22') in production. Two age filters must both pass before a new note is created:

  • The previous shipping note must be at least 27 days old.
  • A diffInMonths check against the previous note must return at least 1 month.

If a shipment that should have been created is missing, check both conditions before assuming a job failure.


X

Graph View