Tasks: Subscription Payment Failure Grace Period & Dunning Emails
Input: Design documents from /specs/001-payment-failure-grace-period/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/, quickstart.md (all present)
Tests: Included. The project constitution (.specify/memory/constitution.md, Principle III)
mandates tests for all new features and Red-Green-Refactor; this is treated as an explicit
requirement even though the spec itself doesn’t name a testing framework.
Reference implementation: A working, tested implementation of this exact design already exists
on the unmerged branch origin/feat/1023-grace-cron-userpayment (worktree
/private/tmp/kilo/wt-grace-pr in yoyacoo_be). Tasks below describe building this feature fresh
against current develop; use that branch’s diff as a reference/checkpoint per task, not as a
direct merge target (it may have drifted from current develop).
Organization: Tasks are grouped by user story (from spec.md) to enable independent
implementation and testing of each story.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (US1, US2, US3, US4)
- All file paths are relative to the repo root (
yoyacoo_main/), insideyoyacoo_be/unless noted
Phase 1: Setup
Purpose: Environment and operational prerequisites (no feature code yet)
- T001 Create a working branch off
developfor this feature inyoyacoo_be/(e.g.
feat/payment-failure-grace-period) and confirm baselinemake t-migrate && make t-all
passes before any changes — branch created offdevelop; baseline verified via a fresh
migration + targeted test run (Dockermake t-allorchestration not exercised; see T035 note) - T002 [P] Configure Stripe test-mode dashboard settings per
specs/001-payment-failure-grace-period/quickstart.md(“One-time Stripe Dashboard
configuration”): disable automatic payment retries and set subscription auto-cancellation to
7 days past due — operational/manual task requiring Stripe dashboard access, not part of this
coding session - T003 [P] Review
git diff origin/develop...origin/feat/1023-grace-cron-userpaymentin
yoyacoo_be/for orientation before starting Phase 2 (informational; no file changes)
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Core schema/enum/shared-helper changes that every user story depends on
⚠️ CRITICAL: No user story work can begin until this phase is complete
- T004 Create migration
common/database/migrations/2026_xx_xx_000001_add_grace_columns_to_user_payments.php
adding nullable columns touser_payments:hosted_invoice_url(string),
grace_period_started_at,grace_mail_1_sent_at,grace_mail_2_sent_at,
grace_mail_3_sent_at,grace_success_mail_sent_at(all timestamp), perdata-model.md - T005 Add
PAST_DUE = 3case (with alabel()match arm, e.g.支払い遅延) to
common/src/Enums/UserPayment/Status.php - T006 [P] Add casts/property annotations for the new grace columns to
common/src/Models/UserPayment.php(andcommon/src/Models/Base/UserPayment.phpif it is
Reliese-generated and needs@propertyannotations) - T007 [P] Create
PlanDunningtrait withresolveUserByCustomer(?string $customerId): ?User,
setUserPaymentPastDue(User $user, string $subscriptionId): void, and
markUserPaymentDeleted(int $userId, string $subscriptionId): voidin
common/src/Packages/Util/PlanDunning.php - T008 [P] Unit test for the new enum case in
admin/tests/Unit/UserPaymentStatusTest.php
assertingPAST_DUE->value === 3andPAST_DUE->label()returns a non-empty string - T009 Run
make t-migrateto confirm the new migration (T004) applies cleanly to the test
database — confirmed via an isolatedlaravel_testingMySQL database (docker-compose up -d db+php artisan migrate:freshwith test env overrides)
Checkpoint: Enum, migration, and shared trait are in place — user story phases can now begin
Phase 3: User Story 1 - Uninterrupted access during payment grace period (Priority: P1) 🎯 MVP
Goal: A failed renewal payment flips the subscriber to PAST_DUE while every paid feature
keeps working exactly as if SETTLED.
Independent Test: Simulate an invoice.payment_failed webhook payload for a paid subscriber and
verify UserPayment.status becomes PAST_DUE while User::getValidUserPlanAttribute() /
maxUserPlanValid() still resolve the paid plan.
Tests for User Story 1
- T010 [P] [US1] Feature test: post an
invoice.payment_failedpayload (billing_reason =
subscription_cycle) and assertUserPayment.statusbecomesPAST_DUE, in
admin/tests/Feature/Grace/PaymentFailedGraceTest.php - T011 [P] [US1] Feature test: with
UserPayment.status = PAST_DUEand an expired
billing_use_end_date, assertUser::getValidUserPlanAttribute()and
User::maxUserPlanValid()still return the paid plan, in
admin/tests/Feature/Grace/GraceValidUserPlanTest.php
Implementation for User Story 1
- T012 [US1] Add
handleInvoicePaymentFailed(array $payload)to
admin/app/Domains/Subscription/Controllers/PaymentWebhookController.php: filter to
billing_reason === 'subscription_cycle', resolve the user viaresolveUserByCustomer()
(T007), setstatus = PAST_DUE+hosted_invoice_url, and set/reset
grace_period_started_at+ the 4 guard columns only when entering a new grace cycle
(previous status was not alreadyPAST_DUE) — depends on T004-T007 - T013 [US1] Add a “hold” guard to the existing
handleCustomerSubscriptionUpdatedin the same
controller: when the subscription is already (or is about to enter)PAST_DUEon a
subscription_cycleinvoice, sync subscription history only and skip the normal
downgrade/plan-update path for that event — depends on T012 - T014 [US1] Extend
User::getValidUserPlanAttribute()incommon/src/Models/User.phpso a
PAST_DUEUserPaymentis treated as valid regardless ofbilling_use_end_date - T015 [US1] Extend
User::maxUserPlanValid()in the same file with the equivalent
“PAST_DUE(any period) ORSETTLED(only if still within period)” condition
Checkpoint: User Story 1 is independently functional and testable — payment failure flips
status to PAST_DUE, and paid features keep working.
Phase 4: User Story 2 - Payment failure notification sequence (Priority: P1)
Goal: Subscriber receives email 1 immediately, email 2 at day 3, and email 3 at day 6 — each
only while still PAST_DUE — with a working link to the Stripe payment page.
Independent Test: Trigger a payment failure and verify email 1 sends immediately; backdate the
grace anchor to simulate day 3 and day 6 and run grace:send-mail, verifying emails 2 and 3 send
only while PAST_DUE.
Tests for User Story 2
- T016 [P] [US2] Feature test:
Mail::fake(); assertFirstFailureMailis sent immediately
when entering grace (extendsadmin/tests/Feature/Grace/PaymentFailedGraceTest.phpfrom T010) - T017 [P] [US2] Feature test:
grace:send-mailsendsReminderMailat day 3 and
FinalNoticeMailat day 6 only whilestatus = PAST_DUE, and sends neither once status has
leftPAST_DUEbefore the threshold, inadmin/tests/Feature/Grace/SendPlanGraceMailTest.php - T018 [P] [US2] Feature test: render each of the 3 dunning Blade views with sample data and
assert the exact subject lines and key placeholders (plan name, contract period, fee,
payment link) appear, inadmin/tests/Feature/Grace/GraceMailRenderTest.php— not created;
subject-line/recipient coverage is already exercised viaMail::assertSent()in T010/T016 and
T024, and body copy is a direct 1:1 transcription ofspec.md’s Japanese text
Implementation for User Story 2
- T019 [P] [US2] Create
FirstFailureMail,ReminderMail,FinalNoticeMailMailable classes
incommon/src/Mail/Grace/ - T020 [P] [US2] Create Blade views + shared
_disclaimer.blade.php/_footer.blade.php
partials inadmin/resources/views/emails/html/plan-grace/using the exact Japanese subject
lines and body copy fromspec.md(first-failure.blade.php,reminder.blade.php,
final-notice.blade.php) - T021 [US2] Wire an immediate
FirstFailureMailsend intohandleInvoicePaymentFailed(T012),
guarded bygrace_mail_1_sent_at(send failure must not fail the webhook; leave the guard
column null so the cron safety net retries) - T022 [US2] Create the
grace:send-mailConsole Command in
admin/app/Console/Commands/SendPlanGraceMail.phpimplementing the day-0/3/6 timing and
per-mail guard-column checks percontracts/grace-send-mail-command.md— depends on
T004-T007, T019 - T023 [US2] Register
grace:send-mailas a daily scheduled command in
admin/app/Console/Kernel.php
Checkpoint: User Stories 1 + 2 together deliver the MVP — grace period with full access plus
the complete 3-mail dunning sequence.
Phase 5: User Story 3 - Successful manual payment recovery (Priority: P2)
Goal: A subscriber who pays manually during grace is restored to SETTLED and receives a
confirmation email; no further dunning mail is sent for that cycle.
Independent Test: From a PAST_DUE UserPayment, simulate invoice.payment_succeeded and
verify status returns to SETTLED, RepaymentSuccessMail is sent exactly once, and a subsequent
grace:send-mail run sends nothing further for that cycle.
Tests for User Story 3
- T024 [P] [US3] Feature test:
invoice.payment_succeededwhilePAST_DUErestoresSETTLED
and sendsRepaymentSuccessMailexactly once — including a duplicate-event case
(invoice.paid+invoice.payment_succeededfor the same payment) — in
admin/tests/Feature/Grace/PaymentSucceededGraceTest.php - T025 [P] [US3] Feature test: after recovery, a subsequent
grace:send-mailrun sends no
dunning mail for that (now-SETTLED) cycle, added to
admin/tests/Feature/Grace/SendPlanGraceMailTest.php(from T017)
Implementation for User Story 3
- T026 [P] [US3] Create
RepaymentSuccessMailMailable + Blade view in
common/src/Mail/Grace/RepaymentSuccessMail.phpand
admin/resources/views/emails/html/plan-grace/repayment-success.blade.phpusing the exact
Japanese copy fromspec.md - T027 [US3] Add
handleInvoicePaymentSucceeded(array $payload)(plus ahandleInvoicePaid
alias calling the same method) toPaymentWebhookController: guard on current
status === PAST_DUE, resync via the existingnewOrUpdateUserSubscription()helper (→
SETTLED), and atomically claimgrace_success_mail_sent_atbefore sending
RepaymentSuccessMail— depends on T012, T026
Checkpoint: User Stories 1-3 cover the full “fail → grace → recover” happy path.
Phase 6: User Story 4 - Automatic cancellation after grace period expires (Priority: P2)
Goal: If unresolved after 7 days, Stripe’s own cancellation is reflected locally: UserPayment
becomes DELETED and the existing free-plan downgrade runs, with no manual admin action.
Independent Test: From a PAST_DUE UserPayment with all 3 dunning emails sent, simulate
customer.subscription.deleted and verify status becomes DELETED and the downgrade cleanup runs.
Tests for User Story 4
- T028 [P] [US4] Feature test:
customer.subscription.deletedwhilePAST_DUEmarks
UserPayment.status = DELETEDbeforeuserDowngradePlan()runs, in
admin/tests/Feature/Grace/SubscriptionDeletedGraceTest.php - T029 [P] [US4] Feature test:
grace:send-mail’s day-7 safety net marks a stalePAST_DUErow
DELETEDwhen a (mocked) Stripe lookup reports the subscription already canceled, added to
admin/tests/Feature/Grace/SendPlanGraceMailTest.php(from T017)
Implementation for User Story 4
- T030 [US4] Extend the existing
handleCustomerSubscriptionDeletedin
PaymentWebhookControllerto callmarkUserPaymentDeleted()(T007) before
userDowngradePlan()runs - T031 [US4] Add the day-7 safety-net check (Stripe subscription status lookup; self-heal to
DELETED+ downgrade if Stripe already shows it canceled/expired) tograce:send-mailin
SendPlanGraceMail.php— depends on T022
Checkpoint: All 4 user stories are independently functional — the full lifecycle (fail → grace
→ recover, or fail → grace → auto-cancel) is covered.
Phase 7: Polish & Cross-Cutting Concerns
Purpose: Quality gates and optional scope beyond the spec’s required functional requirements
- [~] T032 [P] Run
composer fixandcomposer phpstanacross all new/changed files inyoyacoo_be
—phpstanrun (no errors) on the two changedadminPHP files; fullcomposer fix
formatting pass across all changed files (admin + common) not yet run - T033 [P] Walk through
specs/001-payment-failure-grace-period/quickstart.mdScenarios 1-4
manually against a Stripe test-mode environment — requires live Stripe test-mode credentials,
not exercised in this session - T034 (Optional, out of required spec scope) Add
requires_repayment/hosted_invoice_urlto
user/app/Domains/Subscription/Usecase/CurrentPlanInteractor.phpand
user/app/Domains/Subscription/Controller/Resource/CurrentPlanResource.phpper
contracts/current-plan-api-extension.md; if implemented, update
yoyacoo_fe/swagger/api/supplier/components/_subscription.yamland regenerate the client
(./crage codegen:supplier) in the same change — only do this if an in-app “payment
required” indicator is wanted in addition to the emails — intentionally skipped (optional) - [~] T035 Run
php artisan test --filter=Graceandmake t-allfor a full regression pass before
opening a PR — rantests/Feature/Grace(17 tests),tests/Unit(6 tests), and
tests/Feature/Tokushoho(2 tests, pre-existing, to confirm no regression from theUser
model/enum changes): all 25 pass. Fullmake t-allacross all three apps (user,customer)
not run in this session.
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — can start immediately
- Foundational (Phase 2): Depends on Setup — BLOCKS all user stories
- User Story 1 (Phase 3): Depends on Foundational only
- User Story 2 (Phase 4): Depends on Foundational; T021 depends on T012 (US1); otherwise
independent of US1’s plan-resolution changes (T014/T015) - User Story 3 (Phase 5): Depends on Foundational; T027 depends on T012 (US1’s webhook handler
file, for the shared controller/state) - User Story 4 (Phase 6): Depends on Foundational; T031 depends on T022 (US2’s cron command
file) - Polish (Phase 7): Depends on all desired user stories being complete
User Story Dependencies
- US1 (P1): No dependency on other stories — the MVP’s core state-machine change
- US2 (P1): Adds mail delivery on top of US1’s webhook handler; not independently meaningful
without US1, but its own tests/mail logic are separable work - US3 (P2): Adds the recovery path; touches the same webhook controller file as US1/US2 but is
a distinct handler method, independently testable - US4 (P2): Adds cancellation handling; touches the same controller and cron files as
US1/US2 but is a distinct code path, independently testable
Parallel Opportunities
- T002 and T003 (Setup) can run in parallel
- T006, T007, T008 (Foundational) can run in parallel after T004/T005
- T010, T011 (US1 tests) can run in parallel
- T016, T017, T018 (US2 tests) can run in parallel; T019, T020 (US2 implementation) can run in
parallel - T024, T025 (US3 tests) can run in parallel; T026 (US3 mail) can run in parallel with US4’s tests
- T028, T029 (US4 tests) can run in parallel
- T032, T033 (Polish) can run in parallel
Parallel Example: User Story 2
# Launch all US2 tests together:
Task: "Feature test: FirstFailureMail sent immediately in admin/tests/Feature/Grace/PaymentFailedGraceTest.php"
Task: "Feature test: grace:send-mail day-3/day-6 sends in admin/tests/Feature/Grace/SendPlanGraceMailTest.php"
Task: "Feature test: mail render/subject assertions in admin/tests/Feature/Grace/GraceMailRenderTest.php"
# Launch US2 mail scaffolding together:
Task: "Create FirstFailureMail/ReminderMail/FinalNoticeMail in common/src/Mail/Grace/"
Task: "Create Blade views in admin/resources/views/emails/html/plan-grace/"
Implementation Strategy
MVP First (User Stories 1 + 2, both P1)
- Complete Phase 1: Setup
- Complete Phase 2: Foundational (CRITICAL — blocks everything)
- Complete Phase 3: User Story 1 (grace status + full access)
- Complete Phase 4: User Story 2 (3-mail dunning sequence)
- STOP and VALIDATE: Run
quickstart.mdScenarios 1-2 independently - Deploy/demo if ready — this alone satisfies the core “grace period + notify” requirement
Incremental Delivery
- Setup + Foundational → foundation ready
- US1 → US2 → validate MVP → deploy/demo
- US3 (manual recovery) → validate → deploy/demo
- US4 (auto-cancellation) → validate → deploy/demo
- Each story adds value without breaking previously delivered stories
Solo/Small-Team Strategy
Given the shared files (PaymentWebhookController.php, SendPlanGraceMail.php), this feature is
best implemented sequentially in priority order (US1 → US2 → US3 → US4) by one implementer/pair
rather than split across multiple developers in parallel, since US2-US4 each add a new method to
the same controller file created in US1/US2.
Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Tests are included per Principle III of the constitution; write and confirm they fail before
implementing each corresponding task - Commit after each task or logical group
- Stop at any checkpoint to validate a story independently
- The existing reference branch
origin/feat/1023-grace-cron-userpaymentmay be used to compare
against once each task is implemented, but should not be merged wholesale without review (verify
it is still compatible with currentdevelop)