Skip to content

Sync fluent SDK to OpenAPI spec: catalogue lifecycle, checkout locale, customer recovery, test helpers - #96

Merged
sandervanhooft merged 6 commits into
mainfrom
sync/openapi-2026-08
Aug 31, 2026
Merged

sandervanhooft merged 6 commits into
mainfrom
sync/openapi-2026-08

Conversation

@sandervanhooft

Copy link
Copy Markdown
Member

Draft / do not merge before api-php. This is the middle layer of the
chain vatly-api-php -> vatly-fluent-php -> vatly-laravel. It builds
on vatly-api-php PR #56 (sync/openapi-2026-08) and requires it to be
released as v0.1.0-alpha.25 before this can merge / CI can resolve deps.

Wraps the new api-php surface in the fluent SDK's own idioms, delegating to the
updated base client and reconciled against the authoritative
openapi.bundled.yaml. Follows api-php's lead on what is typed vs. untyped.

What's implemented, by feature

Catalogue management

  • New Vatly\Fluent\Catalogue\OneOffProductService and SubscriptionPlanService,
    reachable via $vatly->oneOffProducts() / $vatly->subscriptionPlans()
    (api-only, no Wiring required). Each exposes create, find, update,
    archive, unarchive, list, delegating to api-php's endpoints and returning
    the hydrated OneOffProduct / SubscriptionPlan resources.
  • Those resources now carry the new fields taxBehavior, productType,
    archivedAt, pendingUpdates, updateStatus (plus isArchived()), all from
    api-php - the fluent services surface them by returning the real resource.
  • Rationale for the design: these ops map 1:1 to api-php endpoint methods, so a
    thin composed service (like CustomerService) delegating directly to the
    client endpoint keeps the surface discoverable without a per-op Action
    explosion. Tested by mocking the endpoint on the client (the repo's existing
    pattern, cf. UpdateCustomerTest).

Checkout locale

  • withLocale(?string) on CheckoutBuilder and SubscriptionBuilder
    (threaded into the checkout payload). Accepts a bare code / BCP 47 tag / POSIX
    locale; omitted by default so the checkout detects from the browser.

Subscription scheduledUpdate

  • scheduledUpdate(): ?object and hasScheduledUpdate(): bool on
    SubscriptionHandle (live GET). Untyped (stdClass|null), mirroring
    api-php's Subscription::$scheduledUpdate - see deferral note.

Customer recovery

  • CustomerService::findByEmail() (returns the CustomerCollection) and
    findOneByEmail() (first match or null), backed by a new
    ListCustomersByEmail action and Vatly::listCustomersByEmail().

Test helper: fast-forward-renewal forced outcome

  • $vatly->testHelpers() -> Vatly\Fluent\TestHelpers with advanceRenewal(),
    forceRenewalPaid(), forceRenewalFailed(?string $failureReason), and a
    general fastForwardRenewal($id, $body) passthrough - over api-php's
    testHelpers->fastForwardSubscriptionRenewal($id, [...]).

Webhooks: 10 new catalogue events

  • The one_off_product.* / subscription_plan.* events (update_submitted,
    update_approved, update_rejected, archived, unarchived x2) flow through
    the WebhookProcessor unchanged: they are recorded and dispatched as
    UnsupportedWebhookReceived (the api-php factory's fallback). No production
    code change was needed; a data-provider test locks the behaviour in.

Deferred / checklist

  • Typed catalogue-event DTOs - blocked on the vatlify spec. The persisted
    WebhookEvent.object oneOf/discriminator omits OneOffProduct /
    SubscriptionPlan even though the eventName enum includes the 10 events, so
    there is no spec-defined payload to type against. Deliberately left untyped in
    all three layers (api-php, here, laravel) so typed handling can light up later
    without breaking consumers who match on $event->eventName now.
  • Typed scheduledUpdate / pendingUpdates - same spec gap; surfaced as
    fluent-accessible but untyped stdClass|null, matching api-php.

Dependency wiring

Bumps vatly/vatly-api-php from ^0.1.0-alpha.24 to ^0.1.0-alpha.25 (the
release that will carry api-php #56). Locally this was built against a path
repository symlinking the api-php sync/openapi-2026-08 branch; that local
repo/*@dev constraint was reverted before committing (only the genuine version
bump is committed). composer.lock is gitignored, so nothing pins the symlink.

Tests & quality

  • composer test - 270 passing (up from 228), 623 assertions.
  • composer analyse (phpstan level 6) - no errors.
  • Built and verified against the api-php feature branch.

Sander van Hooft added 2 commits August 31, 2026 14:59
…locale, customer recovery, test helpers)

Expose the new api-php surface (PR #56, released as alpha.25) through the
fluent SDK's own idioms:

- Catalogue management: OneOffProductService + SubscriptionPlanService via
  Vatly::oneOffProducts() / ::subscriptionPlans() (create/find/update/archive/
  unarchive/list). Returns api-php resources carrying the new fields
  taxBehavior, productType, archivedAt, pendingUpdates, updateStatus.
- Checkout locale: withLocale() on CheckoutBuilder and SubscriptionBuilder.
- Subscription scheduledUpdate: scheduledUpdate() / hasScheduledUpdate() on
  SubscriptionHandle (live GET; untyped stdClass|null, mirroring api-php).
- Customer recovery: CustomerService::findByEmail() / findOneByEmail() backed
  by a new ListCustomersByEmail action, plus Vatly::listCustomersByEmail().
- Test helper: Vatly::testHelpers() -> TestHelpers with advanceRenewal /
  forceRenewalPaid / forceRenewalFailed(reason) over fast-forward-renewal.
- Webhook: the 10 catalogue events flow through the WebhookProcessor and are
  recorded + dispatched as UnsupportedWebhookReceived (typed DTOs deferred,
  blocked on the vatlify spec gap - same as api-php).

Bumps vatly/vatly-api-php constraint to ^0.1.0-alpha.25.
CI failed at composer install: the required ^0.1.0-alpha.25 base client is
not published yet (max is alpha.24). Point the dependency at api-php PR #56's
branch (sync/openapi-2026-08) via a VCS repository + inline alias so CI can
install and validate against the real new surface across all PHP versions.

TEMPORARY: revert to the plain "^0.1.0-alpha.25" constraint (and drop the
repositories entry) once vatly-api-php #56 is merged and released as alpha.25.
@sandervanhooft

Copy link
Copy Markdown
Member Author

⚠️ Temporary CI scaffold (this commit): CI was red because the required base client vatly/vatly-api-php ^0.1.0-alpha.25 isn't published yet (max is alpha.24), so composer install couldn't resolve. I've pointed the dependency at api-php PR #56's branch (sync/openapi-2026-08) via a VCS repository + inline alias so CI can validate against the real new surface across all PHP versions.

Before this leaves draft / merges: revert composer.json to the plain "vatly/vatly-api-php": "^0.1.0-alpha.25" constraint and drop the repositories entry, once api-php #56 is merged and released as alpha.25. This PR stays gated on that release.

…eleased)

vatly/vatly-api-php v0.1.0-alpha.25 is released and ships typed webhook DTOs
for the 10 catalogue events, so the fluent WebhookProcessor (which delegates to
api-php's WebhookEventFactory) now surfaces them typed automatically:

- OneOffProductUpdateSubmitted / UpdateApproved / UpdateRejected / Archived /
  Unarchived (carry oneOffProductId, testmode, oneOffProduct resource)
- SubscriptionPlanUpdateSubmitted / UpdateApproved / UpdateRejected / Archived /
  Unarchived (carry subscriptionPlanId, testmode, subscriptionPlan resource)

Changes:
- Revert the CI scaffold in composer.json: drop the vcs `repositories` entry
  and restore the plain Packagist constraint `^0.1.0-alpha.25`.
- Rewrite the WebhookProcessor data-provider test: the 10 catalogue events now
  assert the typed DTOs instead of UnsupportedWebhookReceived.
- README: document the typed catalogue events; drop the deferral note.

composer test: 270 passing. composer analyse (phpstan L6): clean.
@sandervanhooft
sandervanhooft marked this pull request as ready for review August 31, 2026 14:27
@sandervanhooft

Copy link
Copy Markdown
Member Author

Un-drafted: the release gate is cleared. vatly/vatly-api-php v0.1.0-alpha.25 is tagged and on Packagist, so the temporary CI scaffold (the vcs repositories entry + dev-sync/openapi-2026-08 as … constraint) has been reverted — composer.json now depends on the plain released ^0.1.0-alpha.25.

alpha.25 also ships typed webhook DTOs for the 10 catalogue events (OneOffProductUpdateSubmitted … SubscriptionPlanUnarchived). Since the fluent WebhookProcessor delegates to api-php's WebhookEventFactory, these now flow through typed automatically (each carrying the hydrated OneOffProduct/SubscriptionPlan resource). Updated the webhook data-provider test to assert the typed DTOs (it previously asserted UnsupportedWebhookReceived) and dropped the deferral note from the README. The only remaining untyped passthrough is subscription scheduledUpdate, which is still a genuine spec gap.

composer test: 270 passing. composer analyse (phpstan L6): clean.

…elpers (#68), doc effectiveAt

Doc fix — scheduledUpdate field list:
- Add `effectiveAt` (the next-renewal date the change applies; nullable) to the
  ScheduledSubscriptionUpdate field enumerations in README.md and
  docs/webhook-flow.md, matching the OpenAPI spec's required set.

#95 — catch (VatlyException) now covers API-layer errors:
- Add Vatly\Fluent\Exceptions\ApiCallFailedException (extends RuntimeException,
  implements VatlyException); wraps api-php's ApiException as $previous,
  preserving code + message, with apiException() to reach the original.
- Add Concerns\GuardsApiCalls trait; route every fluent->api-php boundary
  through it: all BaseAction subclasses (incl. CreateCustomer's non-duplicate
  path), the catalogue services (create/find/update/archive/unarchive/list),
  and testHelpers(). SubscriptionHandle is covered transitively via its actions.
- Vatly::customerIdFromCheckout() now catches ApiCallFailedException|ApiException
  (code preserved), so its 404->null behaviour holds for both wrapped (prod) and
  bare (mocked) exceptions.
- Document on the VatlyException interface that API transport/HTTP errors are
  included; add an "Error handling" section to the README.

#68 — customer name/email read + update helpers (api-php prerequisite shipped):
- Add Vatly\Fluent\Data\UpdateCustomerData DTO (name?, email?; toPayload strips
  nulls, per the repo's DTO convention).
- CustomerService::update() now accepts UpdateCustomerData|array; add
  CustomerService::identity() read-back (name + email) over the Customer resource.
- README documents both.

composer stays on released ^0.1.0-alpha.25 (no scaffold).
composer test: 286 passing (was 270). composer analyse (phpstan L6): clean.
@sandervanhooft

Copy link
Copy Markdown
Member Author

Follow-up pushed (560362f), three items folded in:

Doc — effectiveAt: added effectiveAt (next-renewal date the change applies; nullable) to the ScheduledSubscriptionUpdate field lists in README.md and docs/webhook-flow.md, matching the spec's required set.

#95 — catch (VatlyException) now covers API errors: new ApiCallFailedException (implements VatlyException) wraps api-php's ApiException as $previous, preserving code + message (getCode() === 404 still works; original reachable via ->apiException()). A GuardsApiCalls trait routes every fluent→api-php boundary through the wrapper: all BaseAction subclasses (incl. CreateCustomer's non-duplicate path), the catalogue services, and testHelpers(); SubscriptionHandle is covered transitively via its actions. customerIdFromCheckout() now catches ApiCallFailedException|ApiException so its 404→null path holds for both wrapped (prod) and mocked (bare) exceptions. Interface docblock + a README "Error handling" section document it.

#68 — customer identity read + update (api-php prerequisite shipped in alpha.25): new Vatly\Fluent\Data\UpdateCustomerData DTO (name?/email?, nulls stripped); CustomerService::update() now accepts the DTO or an array, and CustomerService::identity() reads name+email back off the Customer resource.

composer stays on released ^0.1.0-alpha.25 (no scaffold). composer test: 286 passing (was 270); composer analyse (phpstan L6): clean.

Note on #40 (CustomerHandle): left open. This work adds identity ops on CustomerService (the collection-style service), not a per-customer CustomerHandle; #40's DoD specifically asks for a CustomerHandle class + Vatly::customer($id) accessor. Its trigger (a customer-level mutation op landing at Vatly) is now met by UpdateCustomer, so #40 is unblocked/actionable, but not implemented here.

Sander van Hooft added 2 commits August 31, 2026 17:10
…tionUpdate (alpha.26)

vatly/vatly-api-php v0.1.0-alpha.26 makes Subscription::$scheduledUpdate a typed
ScheduledSubscriptionUpdate (all 8 fields, incl. effectiveAt), replacing the
prior stdClass passthrough.

- Bump the constraint to ^0.1.0-alpha.26.
- SubscriptionHandle::scheduledUpdate() now returns ?ScheduledSubscriptionUpdate
  instead of ?object; hasScheduledUpdate() unchanged (null check).
- Update the tests to build the typed DTO and assert the class + effectiveAt.
- README: scheduledUpdate() described as typed (was "untyped stdClass|null").

composer test: 286 passing. composer analyse (phpstan L6): clean.
Merchants say "Products" / "Manage Products", not the internal "catalogue"
jargon. Since #96 is pre-release (nothing shipped), rename the developer-facing
namespace too for consistency; the public accessors ($vatly->oneOffProducts() /
->subscriptionPlans()) already used product names and are unchanged.

- Namespace Vatly\Fluent\Catalogue -> Vatly\Fluent\Products (git mv of
  src/Catalogue -> src/Products and tests/Catalogue -> tests/Products; updated
  namespaces + `use` statements + test namespaces).
- README + docs: "Catalogue management" -> "Managing products", "catalogue
  events" -> "product events", and all other catalogue/catalog wording.
- Reword catalogue mentions in docblocks (Vatly, the two services,
  GuardsApiCalls, VatlyException).
- Rename the webhook test's catalogueEventProvider -> productEventProvider and
  test_it_dispatches_typed_catalogue_events -> ..._typed_product_events.

Webhook event name strings (one_off_product.*, subscription_plan.*) are the API
contract and are untouched.

composer test: 286 passing. composer analyse (phpstan L6): clean.
@sandervanhooft
sandervanhooft merged commit a0b786a into main Aug 31, 2026
5 checks passed
@sandervanhooft
sandervanhooft deleted the sync/openapi-2026-08 branch August 31, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant