### Subscriptions — Project Integration Guide (Pre-Installed as part of the project now)

This document explains how the Subscriptions plugin is installed, configured, and used within this project. It consolidates the up-to-date behaviour, scheduled tasks, routes, and integration points that are already wired into the app.

---

### What the plugin does

- Creates subscriptions from paid orders when the customer has opted into a subscription for an item/size.
- Manages recurring order generation ahead of the next issue date, including optional tokenized payment attempts.
- Tracks subscription lifecycle (active, paused, cancelled, payment-failed, completed, expired) and next-issue schedule.
- Sends time-based email notifications (assessment expiring reminders, subscription update reminders, etc.).
- Provides Admin UI for subscriptions management and a full audit log of subscription-related actions.

---

### Installation and enabling

1. Register service provider (routes + migrations) - Pre-Installed as part of the project now,
   this step is only needed if you're migrating to an older project

- Add to config/app.php providers:
  - \\MtcPharmacy\\Subscriptions\\SubscriptionsServiceProvider::class

2. Run migrations - Pre-Installed as part of the project now,
   this step is only needed if you're migrating to an older project

- php artisan migrate

3. Enable the feature flag

- The plugin is gated by a configuration flag: plugins/mtc_pharmacy_subscriptions/src/config/subscription.php
  - enabled => env('SUBSCRIPTIONS_ENABLED', false)
  - max_reschedule_days => env('SUBSCRIPTION_MAX_RESCHEDULE_DAYS', 45)
- Set in your .env:
  - SUBSCRIPTIONS_ENABLED=true
  - SUBSCRIPTION_MAX_RESCHEDULE_DAYS=45 (optional, defaults to 45 days)

Notes

- The service provider:
  - Loads plugin routes and migrations
  - Extends view paths for Twig templates shipped with the plugin
  - Injects Admin menu items (non-CLI requests only)
  - Listens for Mtc\\Shop\\Events\\OrderPaidEvent to automatically activate a subscription upon payment

---

### Admin UI

Menu entries are auto-injected under the Clinic node (cached to avoid DB hits):

- Clinic > Subscriptions (/admin/subscriptions)
- Clinic > Subscription Logs (/admin/subscription-logs)

If you don’t see these entries, ensure:

- You are not in CLI context
- Cache hasn’t prevented a stale state; try clearing caches
- The “Clinic” menu root exists (the injector will back off if it can’t find it)

---

### Routes overview

Admin

- GET admin/subscriptions → SubscriptionsController@manage (route: admin-subscriptions)
- POST admin/subscriptions/export → SubscriptionsController@export (route: admin-subscriptions-export)
- GET admin/subscriptions/data → SubscriptionsController@data (route: admin-subscriptions-data)
- GET admin/subscriptions/generate-order/{subscription_id} → SubscriptionsController@generate_order (route: admin-subscriptions-generate-order)
- GET admin/subscriptions/edit/{subscription_id?} → SubscriptionsController@edit (route: admin-subscriptions-edit)
- POST admin/subscriptions/edit/{subscription_id} → SubscriptionsController@update (route: admin-subscriptions-update)
- GET admin/subscriptions/items/delete/{subscription_item_id} → SubscriptionsController@delete_item (route: admin-subscriptions-items-delete)
- GET admin/subscriptions/create/{member_id} → SubscriptionsController@create (route: admin-subscriptions-create)
- POST admin/subscriptions/preferences/data → SubscriptionPreferencesController@update (route: admin-subscription-preferences-data-update)
- GET admin/subscriptions/preferences/data/{item_id?} → SubscriptionPreferencesController@get_data (route: admin-subscription-preferences-data)

Admin — Logs

- GET admin/subscription-logs → SubscriptionLogsController@index (route: admin-subscription-logs)
- GET admin/subscription-logs/view/{log_id} → SubscriptionLogsController@view (route: admin-subscription-logs-view)
- GET admin/subscription-logs/subscription/{subscription_id} → SubscriptionLogsController@forSubscription (route: admin-subscription-logs-for-subscription)
- POST admin/subscription-logs/export → SubscriptionLogsController@export (route: admin-subscription-logs-export)

Front-end (authenticated)

- GET subscription/{subscription_id}/update-subscription → SubscriptionsController@update_subscription (route: fe-subscription-update-subscription)
- GET subscription/{subscription_id}/set-paused → SubscriptionsController@set_paused (route: fe-subscription-set-paused)
- GET subscription/{subscription_id}/set-active → SubscriptionsController@set_active (route: fe-subscription-set-active)
- GET subscription/{subscription_id}/set-cancelled → SubscriptionsController@set_cancelled (route: fe-subscription-set-cancelled)
- POST subscription/{subscription_id}/update-subscription-item → SubscriptionsController@update_subscription_item

Misc

- GET product/allowed-subscription-periods/{shop_item_id} → App\\Http\\Controllers\\ShopController@allowed_subscription_periods (route: product-allowed-subscription-periods)

---

### Scheduled tasks (CRON)

Run the Laravel scheduler every minute (example cPanel crontab):

- /usr/local/bin/php /path/to/artisan schedule:run >> /dev/null 2>&1

The application schedules these commands only when subscription.enabled is true:

- subscriptions:generate_from_orders — every minute
  - Creates subscriptions from paid orders where items requested subscriptions
- subscriptions:generate_orders — daily
  - Generates future orders for active subscriptions due soon
- subscriptions:send_expiring_assessment_email — daily
  - Sends reminders when an assessment is approaching expiry (7 days prior)
- subscriptions:send_subscription_update_reminder — daily
  - Sends “update your subscription” reminders ahead of dispatch lead time

Where this is defined

- app\\Console\\Kernel.php (gated by config('subscription.enabled'))

Key scheduling logic

- The Subscription model uses ORDER_CREATE_DAYS_BEFORE_DISPATCH = 3
- GenerateOrdersFromSubscriptionsCommand targets subscriptions with:
  - status = active
  - next_issue_date not null
  - previous_payment_id not null
  - next_issue_date ≤ now + ORDER_CREATE_DAYS_BEFORE_DISPATCH

---

### End-to-end flow

1. Customer requests a subscription

- On add-to-basket, a listener marks basket item flags:
  - has_requested_subscription = true
  - has_requested_subscription_period (e.g., “1 month”, “2 weeks”)
- That purchase_type handling is wired via an Event listener for Mtc\\Shop\\Events\\AddToBasketEvent at the application level, which updates BasketItem fields.

2. Order paid → subscription activated

- The plugin listens to OrderPaidEvent. On payment, the customer’s first subscription is set active and next_issue_date is calculated.

3. Background jobs

- subscriptions:generate_from_orders
  - Finds paid orders with subscription requests and no existing subscriptions_orders link
  - Builds Subscription + SubscriptionItems (preserving size/assessment), sets interval from the selected period, and associates the source order
  - Sets an initial expiry date based on related Assessment if present (defaults to +1 year)
- subscriptions:generate_orders
  - When within the 3-day window before next_issue_date, generates a new order
  - If attempt_token_payment is enabled (internally), it can try to charge using a previous payment method
  - Increments count, advances next_issue_date (or completes if max issues reached)
- Email reminder commands
  - Send expiring assessment and subscription update reminders to the customer

---

### Payment integration

Payment gateway contract

- plugins/mtc_pharmacy_subscriptions/src/Classes/PaymentGatewayInterface.php
  - \_\_construct($order)
  - processPayment()
  - getPreviousPaymentInfo()

Default behaviour in this project

- PaymentService currently instantiates TestPaymentGateway for processing within Subscription::collectPayment and GenerateSubscriptionsCommand
- To integrate a real gateway (e.g., RyftPaymentGateway), implement PaymentGatewayInterface and use PaymentService(new YourGateway($order))

Important fields on Subscription

- previous_payment_id: must be available for auto-payment of repeat orders
- status: active/paused/payment-failed/cancelled/completed/expired
- interval + interval_multiplier: e.g., weeks + 2 → every 2 weeks
- next_issue_date: calculated based on latest order or creation date
- max_number_of_issues: stop after this many issues

Payment-failed handling (project-level example)

- The application may listen for 'subscriptions.paymentFailed' to mark status, email the user, and re-flag basket items as subscription in a retry flow.

---

### Payment integration testing (required)

Payment integration must be fully tested whenever:

- The subscription feature is enabled on a new site
- A new payment gateway is added or an existing one is changed
- The `PaymentService` or `PaymentGatewayInterface` implementation is modified

#### What to test

- **Tokenized repeat payments**: Verify that `previous_payment_id` is stored correctly after the first successful payment and that subsequent auto-generated orders can charge using the stored token.
- **Payment failure handling**: Confirm that failed payments correctly set the subscription to `payment-failed` status, send the failure notification email, and re-flag basket items for retry.
- **Payment retry flow**: Test that a customer can retry a failed payment from the member area and that successful retry reactivates the subscription.
- **Gateway-specific behaviour**: Each gateway (Opayo, Ryft, etc.) has different tokenization and error response formats — test with the actual gateway in sandbox/test mode, not just `TestPaymentGateway`.

#### Important

The default `TestPaymentGateway` always simulates successful payments. It is not sufficient for verifying real payment flows. Before going live, switch to the real gateway implementation and run through the full payment lifecycle in the gateway's test/sandbox environment.

---

### Front-end integration notes

- Allowed subscription periods for a product are derived by configuration and exposed via route product-allowed-subscription-periods/{shop_item_id}
- Item mapping for subscription pricing
  - Shop item method mapSizePricesToSubscriptions() attaches subscriptionPreference (discounts, flags) and calculates discounted prices for sizes; integrate where product data is prepared for Vue
- Components
  - Import and register components from the plugin in your app.js, for example:
    - import mtcSubscriptionOptions from '../../../plugins/mtc_pharmacy_subscriptions/src/assets/js/components/shop/basket/subscription_options'
      Vue.component('subscription-options', mtcSubscriptionOptions)
    - import mtcSubscriptionItem from '../../../plugins/mtc_pharmacy_subscriptions/src/assets/js/components/members/subscription/subscription_item'
      Vue.component('subscription-item', mtcSubscriptionItem)
    - import mtcSubscriptionActions from '../../../plugins/mtc_pharmacy_subscriptions/src/assets/js/components/members/subscription/subscription_actions'
      Vue.component('subscription-actions', mtcSubscriptionActions)
- No symlink is required when importing directly from the plugin path

Other template/helpers updates sometimes required by project conventions

- Add DataTables assets for admin lists if not already present
- Add an ordinal filter to Twig if needed by templates
- Update members area templates (subscriptions list, orders list) to display subscription states
- In places where basket lines are rendered, show an sRx badge when has_requested_subscription is true

---

### Application-level integration points (summarised)

Controller and model hooks (examples from this project)

- MembersController@orders\_\_subscriptions: append subscription edit URLs when editable; expose cancellation_reasons to Twig
- Member model: public function subscriptions() { return $this->hasMany(Subscription::class, 'member_id'); }
- Shop helpers: determine allowed subscription periods based on SubscriptionPreference
- Basket and order processes: update Get_Items to reflect discounted prices when has_requested_subscription and a sub_config exists

Assessment expiry handling

- Subscription expiry_date often mirrors the assessment expiry. The plugin will attempt to fetch a completed assessment and set subscription expiry accordingly; otherwise a default (e.g., +1 year) is used.

---

### Subscription model reference (selected)

- Statuses: pending, active, paused, expired, payment-failed, cancelled, completed
- Intervals: days, weeks, months
- ORDER_CREATE_DAYS_BEFORE_DISPATCH = 3
- Key methods:
  - setActive(), setPaused(), setCancelled($reason), setFailedPayment(), hasFailedPayment()
  - getLatestOrder(), getLatestSuccessfulOrder(), getLatestFailedPayment()
  - getNextIssueDate($force_new_date = false)
  - getNextOrderDate() and getNextIssueDateForReminder()
  - canOrderBeGenerated($print_debug_info = false)
  - generateNewOrder($ignore_checks = false, $attempt_token_payment = false)
  - collectPayment(Order $order)
  - emailSubscriptionUpdates($subject, $action = '', $content_below_basket = '')

---

### Configuration Options

The subscription plugin supports several configuration options in `plugins/mtc_pharmacy_subscriptions/src/config/subscription.php`:

#### enabled
- **Type:** boolean
- **Default:** false
- **Environment Variable:** SUBSCRIPTIONS_ENABLED
- **Description:** Master switch to enable/disable the entire subscription feature

#### max_reschedule_days
- **Type:** integer
- **Default:** 45
- **Environment Variable:** SUBSCRIPTION_MAX_RESCHEDULE_DAYS
- **Description:** Maximum number of days a customer can reschedule their next issue date from the current scheduled date. This prevents customers from pushing their subscription too far into the future.
- **Example:** If set to 45 days and the customer's next issue date is January 1st, they can only reschedule up to February 15th (45 days from January 1st).

To override these values, add them to your `.env` file:
```
SUBSCRIPTIONS_ENABLED=true
SUBSCRIPTION_MAX_RESCHEDULE_DAYS=30
```

---

### Troubleshooting

- Admin menu missing
  - Ensure you’re not running in console mode
  - Confirm a 'Clinic' parent menu exists in the DB
  - Clear caches if the "installed" flags were set during an incomplete state
- Subscriptions not generating from orders
  - Check: paid orders with has_subscription = 1 and at least one item with has_requested_subscription = 1
  - Ensure no existing subscriptions_orders link for that order
  - Verify the scheduler is running and SUBSCRIPTIONS_ENABLED=true
- Repeat orders not created
  - Confirm subscription.status = active, next_issue_date set, previous_payment_id set when auto-charging
  - Confirm timing window (≤ now + 3 days) is met
- Emails not sending
  - Check queue/cron configuration and mailer settings

---

### Quick checklist

- [ ] Service provider added to config/app.php
- [ ] php artisan migrate
- [ ] .env: SUBSCRIPTIONS_ENABLED=true
- [ ] Server cron: php artisan schedule:run every minute
- [ ] FE components imported and used where needed
- [ ] Members area templates updated to show/manage subscriptions
- [ ] Basket and item mapping include mapSizePricesToSubscriptions()
- [ ] Payment gateway configured for tokenized repeats (set previous_payment_id)
- [ ] Email templates checked

---

### Notes on earlier README instructions

- The original symlink step for subscription_preferences.vue.js is not required in this project setup; we import components directly from the plugin sources in app.js.
- Payment gateway interface method names are: processPayment() and getPreviousPaymentInfo() (supersedes references to getPreviousPaymentId()).
- Scheduler frequencies have been aligned with the project’s Kernel (see the schedules section above).

If you follow the above, the subscription feature will run as integrated in this project: subscriptions are generated from paid orders, orders are created on schedule, admin screens and logs are available, and customers receive the appropriate reminders.

### Testing

For comprehensive testing instructions, see **[TESTING.md](TESTING.md)**.

#### Quick Testing Commands

```bash
# Create a test subscription with sample data
php artisan subscriptions:setup-test

# View detailed subscription information
php artisan subscriptions:show <subscription-id> --all

# Generate orders without checking next issue date
php artisan subscriptions:generate_orders --force

# Generate orders and mark as failed (test failure handling)
php artisan subscriptions:generate_orders --force --fail

# Test specific subscription only
php artisan subscriptions:generate_orders --force --subscription-id=<id>
```

**Testing Resources:**
- 📖 [Full Testing Guide](TESTING.md) - Comprehensive manual testing scenarios
- 🔧 Browser URLs: Member area at `/member/orders/subscriptions`, Admin at `/admin/subscriptions`
- 📧 Email Testing: Mailhog at `http://localhost:8025`
