Subscription History Fix Plan
Goal: Fix the three payment-history bugs without expanding support beyond Stripe card payments.
Scope
- Sort
支払履歴by請求日newest first. - Recover card details when a backfilled invoice does not expose a direct payment method.
- Show the plan paid for by each payment, not the user’s current plan.
Non-goal: support PayPal, Link, bank transfer, or other non-card Stripe payment methods.
Current Root Causes
PlanPurchaseHistoryInteractorcallspaginate()withoutorderBy, so billing order is database-dependent.BackfillHistoricalPayments::insertFromInvoice()only reads$invoice->payment->payment_method. Some invoices expose the card throughpayment_intent->payment_methodinstead.PlanPurchaseHistoryCollectionreads$item->userplan, andUserPayment::userplanresolves the currentuser_plansrow by subscription ID. Subscription upgrades overwrite that row, so old payment rows display the current plan.UserPaymentalready stores the historical plan enum inplan, but it does not store the historical monthly/yearly type.
Simplified Design
1. Billing-date ordering
Modify user/app/Domains/Subscription/Usecase/PlanPurchaseHistoryInteractor.php:
return UserPayment::where('user_id', auth()->user()->user_id)
->where('status', Status::SETTLED->value)
->orderByDesc('payment_date')
->orderByDesc('id')
->paginate($interface->getPerPage() ?? 15);
The secondary ID sort makes equal billing timestamps deterministic across pages.
2. Card-only backfill recovery
Modify admin/app/Console/Commands/BackfillHistoricalPayments.php:
- Read the direct invoice payment method first.
- If absent, read the invoice payment intent ID.
- Retrieve the PaymentIntent and read its
payment_methodID. - Retrieve that PaymentMethod.
- Populate the existing card fields:
last4,exp_month_card,exp_year_card,display_brand, andbilling_detail_name. - Only treat
type === 'card'as a supported payment method. If the resolved method is not a card, log it and leave card fields null.
Do not add a generic Stripe payment-method column or change the existing integer enum.
3. Historical plan snapshot
Add one nullable plan_type column to user_payments:
1: monthly2: yearly
Modify these files:
common/database/migrations/2026_08_02_000001_add_plan_type_to_user_payments.phpcommon/src/Models/Base/UserPayment.phpcommon/src/Models/UserPayment.phpcommon/src/Packages/Util/Subscription.phpadmin/app/Console/Commands/BackfillHistoricalPayments.phpuser/app/Domains/Subscription/Controller/Resource/PlanPurchaseHistoryCollection.php
Live subscription writes already calculate the plan type from plan.interval; persist that value as plan_type.
Backfill derives plan_type from the invoice line price recurring interval. If the invoice does not contain a recognizable interval, leave it null and log the invoice rather than using the current plan.
Update the resource:
contentcomes from the payment row’splan.typecomes from the payment row’splan_type.- Do not access
$item->userplanfor history output.
Rows created before the new column will have a null historical type. They must not fall back to the current subscription type.
Tests
Modify user/tests/Feature/PlanPurchaseHistoryActionTest.php:
- Assert newest
payment_dateis returned first. - Assert equal dates use descending ID order.
- Create a current
UserPlanwith a different plan and assert history returns the payment row’splan. - Assert history returns the payment row’s
plan_type.
Modify admin/tests/Feature/BackfillHistoricalPaymentsTest.php:
- Add an invoice with no direct payment method and a payment intent.
- Mock the PaymentIntent to return a card PaymentMethod.
- Assert card details are persisted.
- Assert a non-card PaymentMethod is not displayed as a card.
- Assert
plan_typeis persisted from the invoice interval.
Modify user/tests/Unit/SubscriptionNewOrUpdateUserSubscriptionTest.php:
- Assert live subscription writes persist
plan_type.
Verification
cd yoyacoo_be/user
./vendor/bin/phpunit tests/Feature/PlanPurchaseHistoryActionTest.php
./vendor/bin/phpunit tests/Unit/SubscriptionNewOrUpdateUserSubscriptionTest.php
cd ../admin
./vendor/bin/phpunit tests/Feature/BackfillHistoricalPaymentsTest.php
cd ..
composer phpstan
After deployment:
php artisan migrate --force
php artisan payments:backfill-historical --dry-run
Confirm card records have last4 or display_brand, history is newest first, and old payments do not change when the current plan changes. Run the real backfill only after the dry run has no unexpected errors.