Release 0.2.0: Cashier-style Laravel layer + Subscriptions resource#1
Merged
Conversation
UUIDv5 is the deterministic SHA-1-based variant that the new OperationDerivation primitive uses to derive stable per-purpose idempotency sub-keys and position UUIDs from a single caller-supplied operation id. extractTimestampMs() reads the embedded ms-prefix back out of a UUIDv7 so retry callers can snapshot a "now" timestamp that survives midnight-cross retries. NAMESPACE_VORGIO_OP is locked in as a public constant — changing it later would silently invalidate every previously-issued idempotency key, so it's part of the protocol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OperationDerivation turns one caller-supplied UUIDv7 into three stable correctness primitives: a per-purpose Idempotency-Key (so chained POSTs under one operation each independently dedupe against Vorgio's middleware), per-index position UUIDs, and a snapshotted "now" instant parsed from the UUIDv7's ms-prefix (so retries crossing midnight produce bit-identical bodies). RetryPolicy carries the retry schedule for the upcoming VorgioClient retry wiring: defaults to three attempts with 200/800/3200 ms backoff, configurable so tests can opt into zero-delay retries without slowing the suite. RetryPolicy::disabled() is the explicit one-shot policy used by the existing mock-client helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…methods
VorgioClient now accepts an optional RetryPolicy and retries up to 3
times on transport-level failures and 5xx responses, replaying the
same Idempotency-Key + body each attempt. The auto-generated UUIDv7
key is computed once outside the retry loop so the Vorgio middleware
sees identical headers across attempts and replays its cached 2xx.
4xx is never retried; 429 keeps its existing Retry-After-aware path.
BREAKING: the ?string \$idempotencyKey parameter on Invoices::create,
Invoices::send, Invoices::markPaid, Clients::create, and
Checkouts::create is renamed to ?string \$operationId. The new
parameter must be a UUIDv7; the SDK derives a stable per-purpose
Idempotency-Key from it via OperationDerivation. SDK is pre-1.0, so
breaking changes on a minor bump are permitted by semver convention.
A new AbstractResource::idempotencyHeader() helper centralises the
derivation so each resource method stays a one-liner. Invoices gains
a cancel() method targeting POST /v1/invoices/{id}/cancel for German
Stornorechnung issuance.
VorgioClient::VERSION bumped to 0.2.0. The vorgioMockClient() test
helper defaults to a no-retry policy so existing single-response tests
continue to observe expected failures; retry-specific tests pass a
zero-backoff RetryPolicy explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new Resource\Subscriptions ships under \$vorgio->subscriptions(),
mapping cleanly onto Vorgio's recurring-template verbs:
- start() → POST /v1/checkouts (composite endpoint that
already provisions client + invoice + send;
the new payload key `every` makes it recurring)
- changeCycle() → POST /v1/invoices/{id}/change-cycle
- stop() → POST /v1/invoices/{id}/stop-recurring
stop() sets `every = null` server-side without deleting the template,
preserving the operator's audit trail. changeCycle() mutates only
cadence + next_invoice_at; it cannot touch positions or amounts, so
the audit story for the recurring invoice stays clean.
Per-purpose operationId derivation means a caller can chain
start → changeCycle → stop under one operation id without idempotency
key collisions — each gets a different sub-key via OperationDerivation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vorgio-php gains a thicker Laravel integration that absorbs every
correctness primitive integrators were reinventing by hand. Consumer
Eloquent models use Vorgio\Laravel\Billable and get:
subscribe(string \$every, array \$invoicePayload, array \$clientPayload = []): Subscription
changeBillingCycle(string \$every, ?string \$nextInvoiceAt = null): Subscription
cancelSubscription(string \$childInvoiceStrategy = 'stop-only'): void
createAsVorgioCustomer(array \$clientPayload): VorgioBillable
The operation-id state machine lives entirely inside the trait. A
pending row in vorgio_operations (polymorphically keyed on the
consumer model — not on vorgio_billables, so the first-time subscribe
bootstrap-then-retry case works) carries the UUIDv7 that derives every
Idempotency-Key. Queue retries pick up the same row, reuse the same
operation id, and replay the cached 2xx on the Vorgio side. No
columns are ever added to the consumer's domain tables.
Four auto-loaded migrations create polymorphic Vorgio-owned tables:
vorgio_billables morphTo the consumer model → vorgio_client_id
vorgio_subscriptions recurring-template state
vorgio_operations transient retry-safety state
vorgio_invoices local webhook mirror
cancelSubscription accepts 'stop-only' | 'storno-always' |
'storno-if-unpaid' so consumers can map "cancel my subscription"
to whatever semantics their product needs (SaaS default is the
third).
A new Http\WebhookController is registered at the configured route
when VORGIO_WEBHOOK_SECRET is set. It verifies signatures via the
existing Webhooks::constructEvent, upserts the local mirrors, and
dispatches seven typed Laravel events
(VorgioCustomerCreated, VorgioSubscriptionStarted/Stopped,
VorgioBillingCycleChanged, VorgioInvoiceSent/Paid/Cancelled) for
listeners to consume. Re-delivery is idempotent via updateOrCreate.
VorgioServiceProvider now wires the RetryPolicy into the singleton
from config('vorgio.retry'), loads migrations, registers the webhook
route, and publishes both vorgio-config and the new vorgio-migrations
groups. The config file is rewritten with the webhook + retry blocks
and folds in the dogfood-item-5 preset-comment cleanup.
orchestra/testbench is added as a dev dependency so the Laravel-layer
tests can boot a real Laravel application with an in-memory SQLite,
RefreshDatabase, and route registration assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANGELOG.md ships in Keep-a-Changelog format with the v0.2.0 entry
covering everything in the release: the Subscriptions resource,
Invoices::cancel(), OperationDerivation, the retry policy, the
Cashier-style Laravel layer, and the breaking idempotencyKey →
operationId rename. The migration guide includes a worked
WooCommerce-style pattern (persist UUIDv7 in order meta) and an
explicit note that the vorgio-for-woocommerce plugin needs a
coordinated release before bumping its SDK constraint to ^0.2.0.
Two new examples:
examples/recurring-billing.php — framework-agnostic smoke flow
(start → changeCycle → stop) using one operation id. Mirrors
the style of examples/checkout.php.
examples/cashier-style.php — Laravel-trait sketch covering
subscribe, changeBillingCycle, cancelSubscription, the
cancellation strategies, and event listening.
README gains a "Recurring billing" section after the existing
Idempotency section that documents the operationId pattern, the
internal retry, and the subscriptions() resource. The Laravel
section is extended with the Cashier-style flow on top of the
existing facade documentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Larastan, phpstan-pest, and Pest's PHPStan extensions so the Laravel integration code analyses cleanly under level 5. Annotate Eloquent models with @Property blocks, type the Billable trait's relations with full generics, and drop now-unneeded @var overrides. Real bug fixed in Billable::cancelSubscription: redundant 'storno-if-unpaid' === 'storno-if-unpaid' branch already narrowed by the stop-only early-return. Move postRawWebhook onto TestCase so its $this->call() is typed, and drop the unused postSignedWebhook helper. CLAUDE.md updated to reflect Subscriptions, Support utilities, the operationId pattern, and the Cashier-style Laravel layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rnel imsuperlative/phpstan-pest requires PHP 8.4 but the SDK targets 8.2+, so composer install failed on the 8.2 and 8.3 CI matrix entries. Replace the typed-$this trick with a global postRawWebhook helper that builds a Symfony Request and hands it straight to app(Kernel::class). Larastan's app() return type makes this fully typed without needing PHPStan to understand Pest's runtime $this binding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Vorgio\Laravel\Billabletrait + polymorphic Vorgio-owned tables (vorgio_billables,vorgio_subscriptions,vorgio_operations,vorgio_invoices) + opt-in webhook controller dispatching seven typed Laravel events. Consumer apps never add Vorgio columns to their domain tables.start(),changeCycle(),stop()+ newInvoices::cancel()for Stornorechnung issuance.Idempotency-Key, per-index position UUIDs, and snapshot timestamp. Internal retry policy (3 attempts, 200/800/3200 ms backoff) replays the same key + body on transport / 5xx.?string $idempotencyKey→?string $operationIdon every POST method. Pre-1.0 SDK so the rename ships on this minor bump. Migration guide inCHANGELOG.md(incl. a UUIDv7-in-WooCommerce-order-meta pattern).Test plan
vendor/bin/pest— 94 passed (215 assertions)composer format(PHP-CS-Fixer)larastanto fully resolve)examples/recurring-billing.phpagainst the vorgio.app sandbox once V'sstop-recurring/change-cycle/cancelendpoints ship peraccounting-v2/plans/recurring-billing-redesign-vorgio.mdvorgio-for-woocommerce) against the renamed parameter per its coordinated upgrade plan — note: the plugin currently passes a non-UUIDv7 string and will throwInvalidArgumentExceptionuntil it bumps; coordinate the plugin's release before tagging this SDK.Tagging
Once this PR is merged, tag
v0.2.0onmainand push the tag — Packagist's webhook will pick it up and publish.🤖 Generated with Claude Code