PluginProbe ʕ •ᴥ•ʔ
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.7.2
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.7.2
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.11.1 1.11.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.13.0 2.14.0 2.14.1 2.15.0 2.15.1 2.16.0 2.16.1 2.16.2 2.16.3 2.17.0 2.17.1 2.17.2 2.18.0 2.19.0 2.19.2 2.19.3 2.19.4 2.2.0 2.2.1 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.22.0 2.22.1 2.23.0 2.24.0 2.25.0 2.25.1 2.25.2 2.26.0 2.27.0 2.27.1 2.28.0 2.29.0 2.29.1 2.29.2 2.29.3 2.29.4 2.3.0 2.3.1 2.30.0 2.31.0 2.31.1 2.31.2 2.31.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.40.0 2.40.1 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.9.0 3.0.0 3.0.0-RC1 3.0.0-RC2 3.0.0-beta1 3.0.0-beta2 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.10.0 3.10.1 3.11.0 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.13.4 3.14.0 3.15.0 3.15.1 3.15.2 3.15.3 3.15.4 3.15.5 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.16.6 3.16.7 3.16.8 3.17.0 3.17.1 3.17.2 3.17.3 3.17.4 3.17.5 3.17.6 3.18.0 3.19.0 3.19.1 3.19.2 3.2.0 3.2.1 3.2.2 3.20.0 3.20.1 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 trunk 4.1.0 0.2.19.1 4.1.1 1.0.0 4.2.0 1.0.1 1.0.2
surecart / app / CLAUDE.md
surecart / app Last commit date
helpers 2 days ago routes 3 days ago src 3 days ago CLAUDE.md 1 week ago config.php 1 week ago helpers.php 6 months ago hooks.php 4 months ago
CLAUDE.md
163 lines
1 # SureCart PHP Backend Patterns
2
3 Patterns and conventions for working in the `app/` directory. See root `CLAUDE.md` for architecture overview.
4
5 ## Adding a New Model
6
7 ```php
8 // app/src/Models/MyModel.php
9 namespace SureCart\Models;
10
11 class MyModel extends Model {
12 protected $endpoint = 'my_models'; // -> https://api.surecart.com/v1/my_models
13 protected $object_name = 'my_model'; // request body wrapper key + hook namespace
14 }
15 ```
16
17 The `/v1` segment comes from `RequestService::getBaseUrl()` (`app/src/Request/RequestService.php:131-133`), not from `$endpoint`.
18
19 Three base classes, not two:
20
21 - `Model` — API-backed (96 models).
22 - `DatabaseModel` — WP custom table. **Also requires `protected $table_name`** (e.g. `Integration.php:15`); its inherited `$endpoint` is vestigial, since queries go through `Query::init($this->object_name)->from($this->table_name)` (`DatabaseModel.php:169`).
23 - `ExternalApiModel` — third-party API. Different contract: `protected $base_url` plus `$endpoint`, its own `makeRequest()` with transient caching, never touches `RequestService` or `SURECART_API_URL`. One subclass repo-wide (`IntegrationCatalog.php`).
24
25 Traits live in `SureCart\Models\Traits` and must be imported by FQCN — `use SureCart\Models\Traits\HasDates;`.
26
27 48 traits exist. The ones most used across real models: `HasDates`, `HasCustomer`, `HasPrice`, `HasCheckout`, `HasProduct`, `HasSubscription`, `HasPurchases`, `HasLineItem`, `HasAffiliation`, `CanFilter`, `HasBillingAddress`, `HasShippingAddress`, `HasPaymentIntent`, `HasPaymentMethod`, `HasImageSizes`, `CanDuplicate`, `HasOrder`, `HasCharge`, `HasLicense`, `CanResendNotifications`, `SyncsCustomer` (the Customer↔WP_User sync), `HasAttributes` (fillable/guarded machinery).
28
29 Note the singular/plural pairs are different traits: `HasSubscription` (4 uses) vs `HasSubscriptions` (1), `HasPurchase` (2) vs `HasPurchases` (3). Check which one you actually want. Rarely used but real: `HasDiscount`, `CanFinalize`, `HasProcessorType`, `HasShippingChoices`, `HasCommissionStructure`. `HasServiceFee` exists but is dead.
30
31 ## Adding a New REST Endpoint
32
33 Three-file pattern, then register in `app/config.php`:
34
35 ```php
36 // 1. Model (see above)
37
38 // 2. app/src/Controllers/Rest/MyModelController.php
39 class MyModelController extends RestController {
40 protected $class = MyModel::class;
41 }
42
43 // 3. app/src/Rest/MyModelRestServiceProvider.php
44 class MyModelRestServiceProvider extends RestServiceProvider {
45 protected $endpoint = 'my_models'; // /wp-json/surecart/v1/my_models
46 protected $controller = MyModelController::class;
47 protected $methods = ['index', 'create', 'find', 'edit', 'delete'];
48 }
49
50 // 4. Add MyModelRestServiceProvider::class to 'providers' array in app/config.php
51 ```
52
53 `$methods` supports exactly these five — there is no `batch` or `archive`. **Extra endpoints do not go in `$methods`**; override `registerRoutes()` and call `register_rest_route()` directly (44 providers do this, e.g. `ProductsRestServiceProvider.php:41-54` for `products/{id}/sync/`).
54
55 Most real providers omit `$methods` entirely and inherit the default.
56
57 Permission callbacks are plain `current_user_can()`, but with **one capability verb per callback**, not a single capability for the endpoint:
58
59 ```php
60 get_item_permissions_check -> read_sc_coupons
61 get_items_permissions_check -> read_sc_coupons
62 create_item_permissions_check -> publish_sc_coupons
63 update_item_permissions_check -> edit_sc_coupons
64 delete_item_permissions_check -> delete_sc_coupons
65 ```
66
67 The capability is not always derived from the endpoint name — `manage_sc_shop_settings` (39 uses) and `manage_options` (22) are the two most common strings overall, used by settings and protocol providers.
68
69 **The base class defaults every permission check to `false`** (`RestServiceProvider.php:349-399`), so a provider that omits them registers a fully locked endpoint rather than an open one.
70
71 ## Error Handling
72
73 ```php
74 // Controllers: propagate WP_Error upward — REST layer auto-converts to response
75 $result = MyModel::create($data);
76 if (is_wp_error($result)) {
77 return $result;
78 }
79
80 ```
81
82 The conversion to an HTTP response is WordPress core's (`WP_REST_Server::error_to_response()`), not SureCart's — the route callback just returns the `WP_Error` untouched (`RestServiceProvider.php:245-249`). It carries a correct status because `ErrorsTranslationService::translate()` sets one (`:213-220`).
83
84 **A throwing integration does take down the rest of the chain.** Integration handlers are invoked through `IntegrationService::callMethod()` (`app/src/Integrations/IntegrationService.php:339-367`), which has **no** try-catch. Only one of the five `surecart/purchase_created` dispatch sites wraps the `do_action` (`DraftCheckoutsController.php:92-97`); the other four do not (`DraftCheckoutsController.php:43`, `CheckoutsController.php:159,243`, `AsyncWebhookService.php:102`). Do not assume you are isolated.
85
86 `ErrorsTranslationService` translates API error codes to user messages automatically — every non-200/201 API response is funnelled through it (`app/src/Request/RequestService.php:365-371`), nothing opt-in. Lookup files are in `Support/Errors/Translations/` (`codes.php`, `attributes.php`, `types.php`, `types-replaceable.php`). Do not duplicate translation logic; extend via the `surecart/translated_error` / `surecart/translated_errors` filters.
87
88 ## Adding a New Integration
89
90 **Purchase-sync** integrations extend `IntegrationService` and implement `PurchaseSyncInterface` — 7 of the 24 directories under `app/src/Integrations/` (LifterLMS, LearnDash, LearnDashGroup, TutorLMS, MemberPress, BuddyBoss, User). The rest are not this shape: AffiliateWP extends `\Affiliate_WP_Base`, Yoast extends `NoIndexService`, and the page-builder/SEO integrations are plain service classes.
91
92 ```php
93 // Contract from IntegrationInterface — EIGHT methods, not six:
94 getName(), getModel(), getLogo(), getLabel(), getItemLabel(), getItemHelp(),
95 getItems($items = [], $search = ''), getItem($id)
96
97 // Required from PurchaseSyncInterface:
98 onPurchaseCreated($integration, $wp_user)
99 onPurchaseInvoked($integration, $wp_user)
100 onPurchaseRevoked($integration, $wp_user)
101 ```
102
103 **These are not abstract methods.** `IntegrationService` declares zero abstract methods — the word `abstract` appears once in the file, on the class declaration (`IntegrationService.php:13`). All eight are concrete stubs returning `''` (`:33-104`), satisfying `IntegrationInterface` on the subclass's behalf. So PHP will **not** error if you omit one: the integration silently registers under an empty name/model, and omitting `getItems()`/`getItem()` renders an empty item picker in the admin UI. That is a very different failure mode from a fatal.
104
105 The dispatcher passes a **third** argument the interface does not declare — `$this->$method($integration, $purchase->getWPUser(), $purchase)` (`IntegrationService.php:365`). Every shipped implementation declares only two, so `$purchase` is available but invisible.
106
107 Implementing `PurchaseSyncInterface` also auto-subscribes you to a **fourth** event: `surecart/purchase_updated``onPurchaseUpdated(Purchase $purchase, $request)` (`IntegrationService.php:143`, method at `:179-248`), which fans out to `onPurchaseProductAdded`, `onPurchaseProductRemoved`, `onPurchaseQuantityUpdated` and `onPurchaseProductUpdated`. This is the plan-change path — an integration that ignores it will not revoke access on a product swap.
108
109 - Integration data (product/price/variant -> third-party item mapping) stored in `surecart_integrations` table via `Integration` model (DatabaseModel)
110 - Register service provider in `app/config.php` under `'providers'`
111 - Actions fire from **five** places, not two: `DraftCheckoutsController::finalize()` (`:94`) and `::manuallyPay()` (`:43`), `CheckoutsController::finalize()` (`:243`) and `::manuallyPay()` (`:159`), and the webhook processor (`AsyncWebhookService.php:102`). Do not hook them manually — and when tracing "where does this fire", do not stop at the draft controller
112
113 ## Routes and Middleware
114
115 The dominant pattern in `app/routes/admin.php` is a `->group()` with `->setNamespace()`, and the inner routes discriminated by the `action` URL var — not a flat chain:
116
117 ```php
118 \SureCart::route()
119 ->where( 'admin', 'sc-products' )
120 ->middleware( 'user.can:edit_sc_products' )
121 ->middleware( 'assets.components' ) // loads Stencil component assets
122 ->middleware( 'assets.admin_colors' )
123 ->setNamespace( '\\SureCart\\Controllers\\Admin\\Products\\' )
124 ->group( function () {
125 \SureCart::route()->get()->where( 'sc_url_var', false, 'action' )->handle( 'ProductsController@index' );
126 \SureCart::route()->get()->where( 'sc_url_var', 'edit', 'action' )->handle( 'ProductsController@edit' );
127 \SureCart::route()->get()->where( 'sc_url_var', 'toggle_archive', 'action' )
128 ->middleware( 'archive_model:product' )->handle( 'ProductsController@toggleArchive' );
129 } );
130 ```
131
132 `->setNamespace()` is what makes the bare `'ProductsController@index'` string resolve. Without it that handler will not resolve.
133
134 App-level middleware aliases — this is the complete registry (`app/config.php:384-390`):
135 `archive_model`, `edit_model`, `nonce`, `webhooks`, `assets.components`, `assets.brand_colors`, `assets.admin_colors`. Note `edit_model` is registered but used by **zero** routes.
136
137 Six more come from the framework (`core/core/src/Kernels/KernelsServiceProvider.php:31-36`): `flash`, `old_input`, `csrf`, `user.logged_in`, `user.logged_out`, and `user.can` — the most-used middleware in the routes. The argument after the colon is a capability (`user.can:edit_sc_products`), parsed in `core/core/src/Middleware/UserCanMiddleware.php:41-49`.
138
139 Much of `app/routes/web.php` applies middleware by FQCN rather than alias, so the alias table is only part of the picture.
140
141 ## Webhook Event -> WordPress Action Mapping
142
143 | API Event | WordPress Action |
144 |---|---|
145 | `purchase.created` | `surecart/purchase_created` |
146 | `purchase.revoked` | `surecart/purchase_revoked` |
147 | `purchase.invoked` | `surecart/purchase_invoked` |
148 | `purchase.updated` | `surecart/purchase_updated` |
149 | `customer.updated` | `surecart/customer_updated` |
150 | `account.updated` | `surecart/account_updated` |
151 | `subscription.renewed` | `surecart/subscription_renewed` |
152
153 Price/product CRUD events also fire but are rarely hooked externally.
154
155 The mapping is **derived, not a lookup table**`AsyncWebhookService::createEventName()` (`:117-120`) is `str_replace('.', '_', $type)` prefixed with `surecart/`. So any event follows the same rule and the table above is convenience, not the contract.
156
157 Two gates decide whether a documented event actually fires:
158
159 1. **Subscription allowlist** — only events listed in `app/config.php:451-466` (`'webhook_events'`) are subscribed. This, not the derivation, is the authority on what arrives.
160 2. **Object-model gate**`AsyncWebhookService::handle()` bails before `do_action` if the payload's `object` has no entry in its `$models` map (`:93-99`). An unmapped object is marked processed and **dropped silently**.
161
162 Handlers receive two arguments, `($model, $webhook->data)` (`:102`) — which is why `onPurchaseUpdated()` takes a `$request` second parameter.
163