Subscription History Fix Deployment Runbook
Scope
This deployment updates the BE payment-history flow so that:
- Each payment is keyed by Stripe
invoice_id. - Trial subscriptions without an invoice do not create payment-history rows.
- Unpaid invoices are not recorded as settled payments.
- Historical paid invoices can be backfilled safely.
- Existing legacy rows without an invoice ID are reconciled when the match is unambiguous.
BE branch: fix/payment-history-invoice-keyed-upsert
Pre-deployment Checks
Confirm the BE release contains these files and changes:
common/database/migrations/2026_07_20_000001_add_stripe_invoice_id_to_user_payments_table.phpcommon/src/Models/UserPayment.phpcommon/src/Packages/Util/Subscription.phpadmin/app/Console/Commands/BackfillHistoricalPayments.phpadmin/app/Domains/Subscription/Controllers/PaymentWebhookController.phpuser/app/Domains/Subscription/Usecase/OrderInteractor.php
Confirm the deployment has a database backup and a Stripe API key with permission to read customers, invoices, and payment methods.
Confirm the application is using PHP 8.2 and the expected Stripe account/environment.
Run the BE tests in CI or on a host with PHP available:
cd yoyacoo_be/user
./vendor/bin/phpunit tests/Unit/SubscriptionNewOrUpdateUserSubscriptionTest.php
cd ../admin
./vendor/bin/phpunit tests/Feature/BackfillHistoricalPaymentsTest.php
- Run static analysis:
cd yoyacoo_be
composer phpstan
Deployment Order
Deploy the BE application before running the backfill command.
Deploy the BE release using the normal CI/CD process.
Run the database migration from the
adminapplication:
cd yoyacoo_be/admin
php artisan migrate --force
- Verify the schema:
php artisan tinker --execute="dump(Schema::hasColumn('user_payments', 'stripe_invoice_id'));"
Expected result: true.
- Confirm the command is registered:
php artisan list | grep payments:backfill-historical
Backfill Procedure
Run the dry run first. This reads paid Stripe invoices but does not write payment rows.
cd yoyacoo_be/admin
php artisan payments:backfill-historical --dry-run
Review the output:
scanned=<users> processed=<invoices> skipped=<items> errors=0
Investigate any errors or plan not resolved log entries before continuing. Plan resolution depends on the Stripe product IDs configured in subscription_setting.subscriptions.
For a single account, use:
php artisan payments:backfill-historical --dry-run --user=<user_id>
After the dry run is approved, execute the backfill:
php artisan payments:backfill-historical
For a single account:
php artisan payments:backfill-historical --user=<user_id>
The command is safe to rerun. Invoice IDs are unique and existing invoice-keyed rows are updated rather than duplicated.
Post-deployment Verification
- Confirm the migration index exists:
SHOW INDEX FROM user_payments
WHERE Key_name = 'user_payments_stripe_invoice_id_unique';
- Confirm invoice IDs are unique:
SELECT stripe_invoice_id, COUNT(*) AS total
FROM user_payments
WHERE stripe_invoice_id IS NOT NULL
GROUP BY stripe_invoice_id
HAVING COUNT(*) > 1;
Expected result: no rows.
Verify one account with renewal history in
/user/accounts/plan?tab=3.Verify the displayed settled rows correspond to paid Stripe invoices and that the amount matches
amount_paid.Repeat the command for the same account:
php artisan payments:backfill-historical --user=<user_id>
The second run must not increase the number of rows for any existing stripe_invoice_id.
Verify a trial subscription without an invoice does not create a
user_paymentsrow.Verify a failed or unpaid invoice does not appear as a settled payment.
Monitor application and Stripe webhook logs for:
plan not resolvedambiguous legacy payment rowsfailed to fetch invoiceserrorsreported by the backfill command
Rollback
Application rollback can restore the previous BE code, but do not immediately roll back the migration after payment rows have been written with stripe_invoice_id.
If the application must be rolled back:
- Stop the backfill command.
- Roll back the application release.
- Keep the
stripe_invoice_idcolumn and unique index until a data migration plan is approved. - Do not run the old code against a partially reverted schema.
The migration rollback is only appropriate before this feature has written data:
php artisan migrate:rollback --step=1
Operational Notes
- The backfill reads Stripe invoices per user and may take time for accounts with large invoice histories.
- Run the full backfill during a low-traffic period.
- Do not log or expose Stripe secret keys.
- An ambiguous legacy row is intentionally left unchanged and logged for manual investigation.
- The payment-history API already filters for settled payments; no FE deployment is required.