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/), inside yoyacoo_be/ unless noted

Phase 1: Setup

Purpose: Environment and operational prerequisites (no feature code yet)

  • T001 Create a working branch off develop for this feature in yoyacoo_be/ (e.g.
    feat/payment-failure-grace-period) and confirm baseline make t-migrate && make t-all
    passes before any changes — branch created off develop; baseline verified via a fresh
    migration + targeted test run (Docker make t-all orchestration 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-userpayment in
    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 to user_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), per data-model.md
  • T005 Add PAST_DUE = 3 case (with a label() 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 (and common/src/Models/Base/UserPayment.php if it is
    Reliese-generated and needs @property annotations)
  • T007 [P] Create PlanDunning trait with resolveUserByCustomer(?string $customerId): ?User,
    setUserPaymentPastDue(User $user, string $subscriptionId): void, and
    markUserPaymentDeleted(int $userId, string $subscriptionId): void in
    common/src/Packages/Util/PlanDunning.php
  • T008 [P] Unit test for the new enum case in admin/tests/Unit/UserPaymentStatusTest.php
    asserting PAST_DUE->value === 3 and PAST_DUE->label() returns a non-empty string
  • T009 Run make t-migrate to confirm the new migration (T004) applies cleanly to the test
    database — confirmed via an isolated laravel_testing MySQL database (docker-compose up -d db + php artisan migrate:fresh with 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_failed payload (billing_reason =
    subscription_cycle) and assert UserPayment.status becomes PAST_DUE, in
    admin/tests/Feature/Grace/PaymentFailedGraceTest.php
  • T011 [P] [US1] Feature test: with UserPayment.status = PAST_DUE and an expired
    billing_use_end_date, assert User::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 via resolveUserByCustomer()
    (T007), set status = 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 already PAST_DUE) — depends on T004-T007
  • T013 [US1] Add a “hold” guard to the existing handleCustomerSubscriptionUpdated in the same
    controller: when the subscription is already (or is about to enter) PAST_DUE on a
    subscription_cycle invoice, sync subscription history only and skip the normal
    downgrade/plan-update path for that event — depends on T012
  • T014 [US1] Extend User::getValidUserPlanAttribute() in common/src/Models/User.php so a
    PAST_DUE UserPayment is treated as valid regardless of billing_use_end_date
  • T015 [US1] Extend User::maxUserPlanValid() in the same file with the equivalent
    PAST_DUE (any period) OR SETTLED (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(); assert FirstFailureMail is sent immediately
    when entering grace (extends admin/tests/Feature/Grace/PaymentFailedGraceTest.php from T010)
  • T017 [P] [US2] Feature test: grace:send-mail sends ReminderMail at day 3 and
    FinalNoticeMail at day 6 only while status = PAST_DUE, and sends neither once status has
    left PAST_DUE before the threshold, in admin/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, in admin/tests/Feature/Grace/GraceMailRenderTest.php — not created;
    subject-line/recipient coverage is already exercised via Mail::assertSent() in T010/T016 and
    T024, and body copy is a direct 1:1 transcription of spec.md’s Japanese text

Implementation for User Story 2

  • T019 [P] [US2] Create FirstFailureMail, ReminderMail, FinalNoticeMail Mailable classes
    in common/src/Mail/Grace/
  • T020 [P] [US2] Create Blade views + shared _disclaimer.blade.php/_footer.blade.php
    partials in admin/resources/views/emails/html/plan-grace/ using the exact Japanese subject
    lines and body copy from spec.md (first-failure.blade.php, reminder.blade.php,
    final-notice.blade.php)
  • T021 [US2] Wire an immediate FirstFailureMail send into handleInvoicePaymentFailed (T012),
    guarded by grace_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-mail Console Command in
    admin/app/Console/Commands/SendPlanGraceMail.php implementing the day-0/3/6 timing and
    per-mail guard-column checks per contracts/grace-send-mail-command.md — depends on
    T004-T007, T019
  • T023 [US2] Register grace:send-mail as 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_succeeded while PAST_DUE restores SETTLED
    and sends RepaymentSuccessMail exactly once — including a duplicate-event case
    (invoice.paid + invoice.payment_succeeded for the same payment) — in
    admin/tests/Feature/Grace/PaymentSucceededGraceTest.php
  • T025 [P] [US3] Feature test: after recovery, a subsequent grace:send-mail run 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 RepaymentSuccessMail Mailable + Blade view in
    common/src/Mail/Grace/RepaymentSuccessMail.php and
    admin/resources/views/emails/html/plan-grace/repayment-success.blade.php using the exact
    Japanese copy from spec.md
  • T027 [US3] Add handleInvoicePaymentSucceeded(array $payload) (plus a handleInvoicePaid
    alias calling the same method) to PaymentWebhookController: guard on current
    status === PAST_DUE, resync via the existing newOrUpdateUserSubscription() helper (→
    SETTLED), and atomically claim grace_success_mail_sent_at before 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.deleted while PAST_DUE marks
    UserPayment.status = DELETED before userDowngradePlan() runs, in
    admin/tests/Feature/Grace/SubscriptionDeletedGraceTest.php
  • T029 [P] [US4] Feature test: grace:send-mail’s day-7 safety net marks a stale PAST_DUE row
    DELETED when 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 handleCustomerSubscriptionDeleted in
    PaymentWebhookController to call markUserPaymentDeleted() (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) to grace:send-mail in
    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 fix and composer phpstan across all new/changed files in yoyacoo_be
    phpstan run (no errors) on the two changed admin PHP files; full composer fix
    formatting pass across all changed files (admin + common) not yet run
  • T033 [P] Walk through specs/001-payment-failure-grace-period/quickstart.md Scenarios 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_url to
    user/app/Domains/Subscription/Usecase/CurrentPlanInteractor.php and
    user/app/Domains/Subscription/Controller/Resource/CurrentPlanResource.php per
    contracts/current-plan-api-extension.md; if implemented, update
    yoyacoo_fe/swagger/api/supplier/components/_subscription.yaml and 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=Grace and make t-all for a full regression pass before
    opening a PR — ran tests/Feature/Grace (17 tests), tests/Unit (6 tests), and
    tests/Feature/Tokushoho (2 tests, pre-existing, to confirm no regression from the User
    model/enum changes): all 25 pass. Full make t-all across 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)

  1. Complete Phase 1: Setup
  2. Complete Phase 2: Foundational (CRITICAL — blocks everything)
  3. Complete Phase 3: User Story 1 (grace status + full access)
  4. Complete Phase 4: User Story 2 (3-mail dunning sequence)
  5. STOP and VALIDATE: Run quickstart.md Scenarios 1-2 independently
  6. Deploy/demo if ready — this alone satisfies the core “grace period + notify” requirement

Incremental Delivery

  1. Setup + Foundational → foundation ready
  2. US1 → US2 → validate MVP → deploy/demo
  3. US3 (manual recovery) → validate → deploy/demo
  4. US4 (auto-cancellation) → validate → deploy/demo
  5. 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-userpayment may be used to compare
    against once each task is implemented, but should not be merged wholesale without review (verify
    it is still compatible with current develop)