Support #10285 ยป SUBSCRIPTION_END_TO_END_REVIEW_2026-08-10.md
Subscription End-to-End Review
Review date: August 10, 2026
Branch: main
Scope: Contract creation, Stripe payments and webhooks, renewals, automatic visit/job generation, cancellation, and failed payments.
Code modifications during review: None.
Executive Summary
The current subscription implementation cannot be classified as fully working or production-safe.
- Normal manual invoice-payment job creation is commented out.
- Service subscription payments have a separate Stripe
invoice.paidauto-job implementation, but some failures can permanently prevent jobs from being created. - Product subscription payments create order-tracking entries, but the delivery-job creation block is commented out.
- Most webhook processing failures are returned to Stripe as HTTP 200, preventing automatic retries.
- Cancellation can be shown as successful locally even when cancellation failed in Stripe.
- Product failed-payment handling may ignore Stripe invoices that use the newer nested subscription reference.
Critical Findings
1. Stripe webhook failures are acknowledged as successful
Both service and product webhook controllers return HTTP 200 for most handler errors.
Files:
evergreen_pos_be/src/controllers/serviceWebhook.controller.js:143evergreen_pos_be/src/controllers/webhook.controller.js:77
Impact:
Stripe sends invoice.paid
-> database, invoice, or job operation fails
-> backend returns HTTP 200
-> Stripe considers the event delivered
-> Stripe does not retry
-> payment may succeed while local processing remains incomplete
This can affect contract creation, invoice recording, renewal processing, auto-job generation, failed-payment handling, and cancellation finalization.
2. Service auto-job lock can permanently block job creation
The service invoice.paid handler sets initialJobsScheduled = true before validating the subscription plan, variant, or services.
File:
evergreen_pos_be/src/service-webhook-handlers/invoicePaid.js:65
The function then returns normally when:
- The subscription plan cannot be found.
- The selected variant cannot be found.
- Neither the quotation nor plan contains usable services.
Files:
evergreen_pos_be/src/service-webhook-handlers/invoicePaid.js:88evergreen_pos_be/src/service-webhook-handlers/invoicePaid.js:97evergreen_pos_be/src/service-webhook-handlers/invoicePaid.js:182
Because these paths return instead of throwing an error, the lock is not reverted.
Payment succeeds
-> initialJobsScheduled lock is acquired
-> plan, variant, or service lookup fails
-> zero jobs are created
-> initialJobsScheduled remains true
-> subsequent attempts skip job generation permanently
3. An empty visit schedule is treated as successful zero-job scheduling
When visitSchedule exists but contains no month entries, the helper returns zero dates with hasSchedule: true.
Files:
evergreen_pos_be/src/utils/visitScheduleHelper.js:94evergreen_pos_be/src/service-webhook-handlers/invoicePaid.js:341
The caller can then:
- Create zero jobs.
- Set
invoice.jobCreated = true. - Keep
contract.initialJobsScheduled = true. - Send a jobs-scheduled notification reporting zero jobs.
This behavior was reproduced through a targeted helper execution.
4. Product subscription delivery-job creation is disabled
Product subscription renewals create an order-tracking entry, but the actual Job.create() implementation is commented out.
Files:
evergreen_pos_be/src/webhook-handlers/invoicePaid.js:173evergreen_pos_be/src/webhook-handlers/invoicePaid.js:191
If a delivery or pickup job is expected for every product subscription cycle, that functionality is not active in the current branch.
Lifecycle Status
| Subscription flow | Result | Findings |
|---|---|---|
| Service contract creation | At risk | Contract can be created by checkout completion or the invoice-paid fallback, but handler failures are acknowledged with HTTP 200. |
| Product subscription creation | Partially working | Checkout creates the subscription and order tracking. Most non-race failures are still acknowledged with HTTP 200. |
| Initial Stripe payment | At risk | Service payment can generate jobs, but permanent-lock and zero-job defects exist. |
| Renewal payment | Partially working | Transactions and cycle counters are updated, but product delivery jobs remain disabled. |
| Service visit/job generation | Blocked in some cases | Depends on successful plan, variant, service, address, and schedule resolution on the first processing attempt. |
| Product delivery-job generation | Not active | The job creation code is commented out; only order tracking is created. |
| Cancellation | At risk | Local cancellation can be saved even when Stripe cancellation fails. |
| Failed payments | Incomplete | Product handler can silently ignore newer nested Stripe subscription references. |
| Automatic tenure completion | Present but at risk | Daily cron scheduling exists, but Stripe/API failures can leave local and Stripe states inconsistent. |
Contract Creation Review
Service subscription contracts can be created through:
checkout.session.completed- An
invoice.paidfallback using the pending subscription record
The use of an upsert keyed by stripeSubscriptionId helps prevent duplicate contracts when Stripe events arrive concurrently.
However, configuring the one-year Stripe cancellation date is best-effort. If the Stripe cancel_at update fails, contract creation continues.
File:
evergreen_pos_be/src/service-webhook-handlers/checkoutCompleted.js:251
The local contract may therefore have a one-year end date while Stripe continues charging after that date.
Stripe Payment and Webhook Review
Service subscriptions
The service webhook handles:
checkout.session.completedinvoice.paidinvoice.payment_failed- Invoice-related
payment_intent.payment_failed - Invoice-related
charge.failed customer.subscription.deleted
invoice.payment_succeeded and invoice_payment.paid are intentionally skipped to avoid duplicate handling.
The primary reliability defect is that handler exceptions are acknowledged with HTTP 200. Consequently, Stripe will not retry recoverable database, notification, invoice, or job-generation errors.
Product subscriptions
The product webhook handles:
checkout.session.completedcheckout.session.expiredinvoice.paidinvoice.payment_failedcustomer.subscription.updatedcustomer.subscription.deleted
It returns HTTP 500 only for the recognized initial ordering race where invoice.paid arrives before the local subscription exists. Most other failures receive HTTP 200 and are not retried.
Renewal Review
Service subscriptions
Renewal payments update the invoice and contract cycle information and send payment notifications.
Jobs are normally scheduled for the full configured visit schedule during the initial payment. Later renewals do not recreate jobs after initialJobsScheduled becomes true. Therefore, missing jobs during initial scheduling are not automatically repaired by subsequent renewals.
Product subscriptions
Renewal processing:
- Creates an idempotent transaction.
- Creates an order-tracking entry.
- Increments
cycleNumber. - Updates
nextJobDate. - Sends renewal receipt and notifications.
It does not create a delivery job because that block is commented out.
The next cycle date is calculated by adding fixed values such as 30, 90, or 182 days.
File:
evergreen_pos_be/src/webhook-handlers/invoicePaid.js:227
This can drift from Stripe calendar billing. For example, repeatedly adding 30 days does not consistently preserve a monthly billing day. Stripe invoice period dates would be a more reliable source of truth.
Automatic Service Visit and Job Generation
The service subscription auto-job function:
- Loads the plan and selected variant.
- Resolves company holidays and weekend settings.
- Applies per-day capacity and customer-spacing rules.
- Uses quotation services when available.
- Falls back to services configured on the plan variant.
- Creates one job per service per scheduled visit.
- Attempts to prevent duplicates using subscription, service, and start-date information.
Functional risks:
- The contract lock is acquired too early.
- Missing plan, variant, or services permanently retains the lock.
- An empty schedule is accepted as a valid zero-job result.
- Saving
invoice.jobCreatederrors are logged and ignored. - Webhook-level errors are acknowledged with HTTP 200.
- Later renewals do not repair incomplete initial job generation.
Cancellation Review
Service subscriptions
Service cancellation normally schedules Stripe cancellation at period end.
- Jobs after the cancellation cutoff are changed to
auto cancelled. - Immediate/local cancellation soft-deletes future jobs.
- Reactivation restores
auto cancelledjobs toNew.
However, unexpected Stripe cancellation failures are caught and local processing continues.
Files:
evergreen_pos_be/src/services/subscripiton.service.js:4284evergreen_pos_be/src/services/subscripiton.service.js:4301
The UI can therefore report a scheduled cancellation even while Stripe continues billing.
Job cleanup failures are also caught and returned as zero updated jobs, so cancellation may report success while future jobs remain active.
Product subscriptions
Product cancellation similarly continues with the local status update even if Stripe cancellation fails.
Files:
evergreen_pos_be/src/services/subscripiton.service.js:9031evergreen_pos_be/src/services/subscripiton.service.js:9038
Immediate cancellation changes future delivery-job status to Canceled, but this only helps if delivery jobs were previously created.
Failed-Payment Review
Service subscriptions
The service handler:
- Resolves multiple possible Stripe subscription ID locations.
- Marks the contract
past_due. - Marks a pending local invoice attempt as failed when one exists.
- Sends customer and administrator notifications.
It does not automatically cancel or suspend already-created future service visits. A past-due customer can retain scheduled jobs until handled operationally.
Product subscriptions
The product failed-payment handler reads only:
const stripeSubscriptionId = invoice.subscription;
File:
evergreen_pos_be/src/webhook-handlers/invoicePaymentFailed.js:9
The successful-payment handler already supports newer nested Stripe structures such as parent.subscription_details.subscription, but the failed-payment handler does not.
When Stripe provides only the nested reference, the failed-payment handler exits without:
- Marking the subscription
past_due. - Recording a failed transaction.
- Sending the customer payment-failure email.
- Sending administrator notifications.
Automatic Tenure Completion
A daily cron runs at 1:15 AM using SUBSCRIPTION_END_TZ or America/New_York.
For service subscriptions ending the following day:
- Contracts with a Stripe subscription ID are scheduled to cancel at the custom end date.
- Contracts without a Stripe ID are marked completed locally.
For product subscriptions, the cron requests immediate cancellation.
Risks:
- Stripe scheduling failure is logged but does not automatically retry through a durable queue.
- Webhook finalization errors can be acknowledged with HTTP 200.
- A local completed/cancelled state can become inconsistent with Stripe billing state.
Verification Performed
- Frontend production build: Passed.
- Backend subscription/webhook JavaScript syntax: No syntax defect identified.
- Normal visit-scheduling helper execution: Passed and produced expected business-day dates.
- Empty visit schedule: Confirmed defect; returned zero dates with
hasSchedule: true. - Existing backend Jest suite: Not green.
- Subscription-specific Jest or integration tests: No dedicated test suite exists in the repository.
Real Stripe payments were not executed because safe integration testing requires:
- Stripe test-mode credentials.
- The service and product webhook signing secrets.
- A non-production tenant database.
- Test customers, plans, variants, quotations, services, and invoices.
- A test webhook endpoint accessible to Stripe or Stripe CLI forwarding.
Recommended Priority
Priority 1
- Return a retryable non-2xx response for recoverable webhook failures.
- Acquire or finalize
initialJobsScheduledonly after validating inputs and successfully creating all expected jobs. - Reject zero-job schedules when the plan expects visits.
- Restore product subscription delivery-job creation if delivery jobs are required.
Priority 2
- Do not report cancellation success when Stripe cancellation failed unexpectedly.
- Support nested Stripe subscription references in product failed-payment handling.
- Use Stripe invoice period dates for product renewal scheduling.
- Define whether past-due service subscriptions should suspend future jobs.
Priority 3
- Add automated webhook tests for duplicate delivery, event ordering, database failures, and retries.
- Add end-to-end test fixtures for service and product subscription lifecycles.
Overall Assessment
Subscriptions are not fully verified or fully functional in the current branch.
The highest-priority production risks are:
- Webhook failures being acknowledged as successful.
- Permanent blocking of service auto-job generation.
- Empty schedules being recorded as successful.
- Product subscription delivery jobs not being created.
- Local cancellation succeeding while Stripe cancellation failed.
- Product failed-payment events being silently ignored for nested Stripe invoice structures.