# Migrating from Stripe / Laravel Cashier to Octany

This is the doc you want before scoping a Cashier → Octany migration. It
maps Cashier concepts to Octany ones, calls out where Octany is *not yet*
on parity, and lists the questions you should ask product before
committing.

## TL;DR

- Octany is a **billing engine**, not just a Stripe wrapper. It owns the
  subscription/order state machine, hosted checkout, dunning, billing
  portal, VAT, and supports non-card methods (Swish, Billecta, Trustly,
  Fortnox invoices) that Stripe doesn't.
- For the **happy path** (recurring SaaS, card-only) the moving pieces
  map cleanly. For SCA-heavy custom flows or Stripe-Connect-style
  marketplaces, Octany is not a drop-in replacement today.
- The biggest API gaps to budget around are: no customer-management API,
  no `POST /checkout/sessions`, no `Idempotency-Key`. Workarounds below.

## Concept mapping

| Cashier / Stripe | Octany | Notes |
| --- | --- | --- |
| `Stripe\Customer` | _no public API surface_ | Customer details are captured by the hosted checkout. Correlate via `reference_id` you set on the subscription/order. |
| `Stripe\PaymentMethod` | `BillingMethod` | Polymorphic — supports card, Swish, Billecta, Trustly, Fortnox. |
| `Stripe\Product` + `Stripe\Price` | `Product` (single object) | Octany merges product and price. To "change price", update the product or `POST /subscription/{id}/product` to swap. |
| `Stripe\Subscription` | `Subscription` | One product per subscription. Status enum overlaps but is not identical. |
| `Stripe\Subscription.items[]` | _not modeled_ | Octany subscriptions are single-product. Multi-line behavior is achieved via one-off orders attached to the subscription. |
| `Stripe\Invoice` | `Order` | Each scheduled charge is an order. One-off charges also create orders (`POST /subscription/{id}/order`). |
| `Stripe\Charge` / `PaymentIntent` | `Order.state` + `Order.remote_id` | The Stripe charge ID is on `Order.remote_id`. |
| Stripe Checkout Session | Hosted checkout URL | `https://checkout.octany.com/checkout/{account-uuid}/product/{product-id}?…`. No API session helper yet. |
| Stripe Customer Portal | Octany billing portal URLs | The subscription resource includes signed URLs: `update_card_url`, `invoices_url`, `cancel_url`. |
| `subscription_updated` webhook | `subscription.updated` | Same envelope shape, different event names. See [webhooks.md](webhooks.md). |
| `Idempotency-Key` header | _unsupported_ | Reconcile via list-after-write today. |
| `Stripe::setApiKey()` (account-implicit) | `X-API-KEY` + `{account}` in URL | Octany API keys are scoped to a single account, and the account is in the URL. |
| `cashier:run` artisan jobs | _not needed_ | Octany owns the renewal cycle. |

## Status enum mapping

Only an approximation — the lifecycles are similar but not identical.

| Stripe `status` | Octany `status` | Notes |
| --- | --- | --- |
| `incomplete` | `pending` | Payment not yet received. |
| `incomplete_expired` | `expired` | Caveat: Octany `expired` also covers fixed-cycle completion, not just incomplete-expired. |
| `trialing` | `trialing` | |
| `active` | `active` | |
| `past_due` | `delayed` (transient) → `unpaid` (terminal) | Octany splits "retrying" from "needs admin". |
| `canceled` | `cancelled` | Note British spelling. |
| `unpaid` | `unpaid` | |
| _N/A_ | `future_start` | Octany-specific: subscription scheduled to start in the future. |

## Webhook event mapping

| Stripe | Octany |
| --- | --- |
| `checkout.session.completed` | `order.confirmed` (and shortly after, `order.paid`) |
| `customer.subscription.created` | `subscription.created` |
| `customer.subscription.updated` | `subscription.updated` |
| `customer.subscription.deleted` | `subscription.cancelled` |
| `invoice.paid` | `order.paid` |
| `invoice.payment_failed` | _no direct event_ — observe `subscription.updated` with `status: unpaid`/`delayed` |
| `payment_intent.succeeded` | `order.paid` |
| `product.updated` | `product.updated` |

Octany does not emit a per-attempt failure event today. If your dunning
logic depends on `invoice.payment_failed`, you'll need to subscribe to
`subscription.updated` and react to `status` transitions instead.

## Migration playbook

This is the rough order of operations. None of it is automated by
Octany today; you'll write the migration script.

1. **Inventory Cashier state.** Export `users`, `subscriptions`,
   `subscription_items`, and the underlying Stripe objects. Capture
   `stripe_id`, `stripe_status`, `current_period_end`, `trial_ends_at`,
   `canceled_at`.

2. **Provision Octany products.** For every Stripe `Price` you currently
   sell, create the equivalent Octany `Product` (one per price). Save
   the mapping `stripe_price_id → octany_product_id`.

3. **Migrate customer payment methods.** Octany does **not** import
   Stripe payment methods automatically. Options:
   - Re-collect via the hosted checkout (cleanest, works for SCA/3DS).
   - Use Stripe's PaymentMethod-attach migration to a new Stripe account
     and pair it with Octany's Stripe billing-method type (advanced;
     contact support).

4. **Recreate subscriptions.** For each existing Cashier subscription,
   create the Octany subscription via the hosted checkout (best) or via
   support tooling (for bulk migration). Copy `reference_id =
   stripe_subscription_id` so webhook reconciliation can match
   pre/post-migration records.

5. **Backfill historical data.** Octany doesn't currently expose a
   bulk-import API for orders/invoices. Either keep Cashier read-only
   for history, or work with support on a one-shot import.

6. **Cut over webhooks.** Register the Octany webhook endpoint, verify
   the signature (see [webhooks.md](webhooks.md)), and route events into
   the same application workflow as your Cashier webhooks did. Keep
   Stripe webhooks on for at least one billing cycle to compare.

7. **Decommission Cashier.** Once Octany has driven a full billing
   cycle for every customer, remove `cashier:*` jobs and the `stripe_*`
   columns.

## Code-level adjustments

If your app talks to Cashier through the Eloquent traits, the typical
diff is:

```diff
-use Laravel\Cashier\Billable;
+use App\Octany\Concerns\HasOctanySubscriptions;

 class User extends Authenticatable
 {
-    use Billable;
+    use HasOctanySubscriptions;
 }
```

…where `HasOctanySubscriptions` is a thin wrapper you write that:

- Stores `octany_subscription_id` on the user (and uses your own user ID as `reference_id` for correlation).
- Calls the Octany API via the SDK or Guzzle.
- Translates webhook events into the same domain events your app
  already consumes.

We deliberately don't ship a "Cashier-shaped" Octany trait — the
abstractions don't line up cleanly enough to be worth the lossy
adapter.

## Things to ask product before starting

- Do we need **multi-line invoices**? Octany subscriptions are
  single-product; multi-line is awkward.
- Do we need **proration on plan changes**? `POST
  /subscription/{id}/product` swaps the product, but proration semantics
  may differ from Stripe's defaults.
- Do we need **direct API charges** (no hosted UI)? Octany doesn't
  expose `POST /charges` today.
- Are we using **Stripe Connect / Express**? No equivalent in Octany.
- Do we need **usage-based billing**? Not modeled today.

If the answer to two or more of those is "yes", talk to Octany product
before committing to the migration.
