Skip to content

Releases

1.31.0

Choose a tag to compare

@goldnead goldnead released this 17 Oct 17:33

Changed

  • Settings: the addon now declares settingsNavParent() ([section, name] of its own Control Panel navigation entry). With statamic-brand-context 1.16 or newer its settings show as the sub-item "Einstellungen" under that entry; addons without a navigation of their own share one "Addon-Suite" page under Settings. Older brand-context versions ignore the method and keep the previous layout.

Upgrading

  • No migration, no new config key.

1.30.0

Choose a tag to compare

@goldnead goldnead released this 17 Oct 00:42
305ffcb

Fixed

  • A Mollie webhook no longer answers 200 when Mollie itself cannot be asked (network error,
    timeout, 5xx, 429, 408). MollieGateway::fetch() now throws ProviderUnavailable for those,
    and the webhook endpoint answers 503. Mollie redelivers on anything but a 200 (10 calls over
    26 hours, https://docs.mollie.com/reference/webhooks); the 200 used to end that, and the buyer
    had paid for an order that was never fulfilled. A 404 stays an answer.
  • A double click on „buy" no longer creates two payments. Checkout::start() gives a repeated
    start of the same basket by the same buyer (same address, products, quantities, discount,
    return URL, methods) the open payment of the last ten minutes back, with the same checkout
    URL. The claim is a unique index on the new payments.checkout_key (works on SQLite, where
    lockForUpdate() does nothing), and two requests in flight at once end up on one row.
    Every provider create call also carries an idempotencyKey (Mollie: Idempotency-Key
    header), so a retry after a lost answer gets the first payment back.

Upgrade

  • Run php artisan migrate (adds payments.checkout_key, nullable, unique).
  • A custom PaymentGateway now receives idempotencyKey in the createPayment() payload.
    Drop it or send it as your provider's idempotency header; do not pass it on in a request body.

1.29.4

Choose a tag to compare

@goldnead goldnead released this 11 Oct 17:11
032b214

Changed

  • References to the consent for immediate delivery of digital content now cite § 356 Abs. 6 BGB
    (Abs. 5 covers services): CP section label, merchant hint on a withdrawal, comments and docs.
    No behaviour change.

1.29.3

Choose a tag to compare

@goldnead goldnead released this 06 Oct 17:31

Fixed

  • The Suite licence notice no longer appears inside a framed control panel (an add-on's
    overlay, a live preview). There it covered the frame's own controls, for example Inline
    Edit's Save button. It still appears in the control panel itself.

1.29.2

Choose a tag to compare

@goldnead goldnead released this 26 Sep 02:41

Fixed

  • [fix] A cancelled agreement whose paid term still runs no longer reads „Beendet am " next to „Bezahlt bis ". The portal says „Gekündigt · Läuft bis
    " while the term runs and „Beendet am " afterwards. New
    Subscription::endsAt() (end of the paid term where it lies after the stop, else the stop) and
    Portal\Display::ending($subscription, standalone: false); standalone: true gives the
    sentence on its own line („Gekündigt, läuft bis 26.09.2027") for an app.

Added

  • [new] Cancellations::cancel($subscription, ?$email, array $copies = []): further addresses get the
    same confirmation as a mail of their own (a team's billing address), each once, never the
    recipient again, logged with meta.copy. A copy that fails changes nothing else;
    CancellationOutcome::$copiedTo lists the addresses reached.

1.29.1

Choose a tag to compare

@goldnead goldnead released this 26 Sep 00:15

Changed

  • The cancellation sequence (provider first, confirmation in Textform CancellationConfirmed,
    entry in the payment's log) is one public call now:
    app(Support\Cancellations::class)->cancel(Subscription $subscription, ?string $email = null): Support\CancellationOutcome.
    The portal's CancellationController uses it; an app API ending the same agreement can too,
    without rebuilding the three steps. Who may cancel stays the caller's decision. The outcome
    carries status (cancelled, already_ended, busy, failed), cancelled(), moment,
    until, confirmationSent, name, email. No behaviour change in the portal.

1.29.0

Choose a tag to compare

@goldnead goldnead released this 25 Sep 21:23

Added

  • [new] anrede setting (sie | du, env STATAMIC_PAYMENTS_ANREDE, on the settings screen per brand):
    how the German buyer-facing texts address the buyer. Portal, checkout refusals, withdrawal and
    cancellation pages, their mails, reminders and dunning all follow it. du lines ship in
    lang/de/du/. Finding from ChoirLive: account and team pages said "du", the portal after cancelling
    said "Ihr Vertrag".

Changed

  • Settings screen descriptions (Control Panel) now say "du" like the rest of this addon's CP texts.

Upgrading

  • Default is sie: nothing a buyer reads changes until you pick du. No migration.

1.28.0

Choose a tag to compare

@goldnead goldnead released this 25 Sep 20:12

Findings from the ChoirLive end-to-end check.

Upgrading

  • New config key display_timezone (STATAMIC_PAYMENTS_DISPLAY_TIMEZONE), also on the settings
    screen per brand. Null keeps today's behaviour for most sites: it falls back to legal.timezone,
    then statamic.system.display_timezone, then app.timezone. No migration. Do not change
    app.timezone to fix a displayed time
    : stored timestamps carry no zone and would all shift.
  • Portal texts: new keys cancel_paid_until, cancel_effect_until, cancelled_until,
    mail_cancelled_until; cancel_effect and mail_cancelled_body no longer say "immediately".
    Sites with overridden translations should review them.

Fixed

  • [fix] Control Panel, buyer column: a purchase for an Eloquent user without a morph alias
    (App\Models\User) read "App\ Models\ User Lara Muster". It now reads "User Lara Muster", and a
    purchase for the buyer themselves shows no second name at all (list, subscription list, detail).

  • [fix] Cancellation page: "Beginn" showed starts_at, the day the provider's rhythm starts (for an
    annual plan one year after the purchase). It now shows when the contract began. "Nächste
    Abbuchung" is replaced by "Bezahlt bis".

  • [fix] Cancellation wording: "Die Kündigung wirkt sofort" contradicted the checkout (end of the paid
    term). Page, confirmation screen and confirmation mail now say: no further charge, the contract
    ends when the paid period runs out on . Matches what closeFor() does with the access.

  • [fix] Times in the shop's zone: the cancellation screen and mail, order pages, pause and switch
    notes and the § 312k/§ 356a receipts formatted in app.timezone, so a UTC server wrote
    "19:07 Uhr" for 21:07 in Berlin. All go through LocalTime now; storage stays UTC.

  • [fix] Subscription line name: "ChoirLive Chortarif (jährlich) — jährlich" named the rhythm twice.
    A product name that already contains the rhythm word gets no suffix (Subscriptions::lineLabel(),
    whole word only: "halbjährlich" is not "jährlich"). This is the name the invoice line carries.

Documented

  • The portal needs no confirmed user account: the link goes to the address on the order and
    following it proves the address. A buyer who never confirmed the site's account mail can still
    see orders and cancel (test PortalWithoutConfirmedAccountTest).

1.27.0

Choose a tag to compare

@goldnead goldnead released this 25 Sep 11:52

Upgrading

  • No migration, no new permission, no new config key. Payments without for behave as before,
    except that a refund, chargeback, pause or end now touches only the grants of that purchase.
  • A caller that put entitlement_subject into meta itself (statamic-teams until its next
    release) now gets an InvalidArgumentException; pass for instead.
  • A subclass of EntitlementsBridge that overrides grantSlug(), grantLine() or extendFor()
    must widen string $subject to mixed $subject: the subject is now resolved before it gets there.

Added

  • [new] Buying for someone else (a team): $details['for']. A saved Eloquent model, a Statamic user
    or a SubjectReference, checked at the till (resolvable type, existing record; otherwise the
    address, with a warning). Stored as meta.entitlement_subject = {type, id}, which is now a
    reserved key: passing it inside meta throws. The entitlements bridge then grants, renews,
    pauses, closes and revokes for that subject instead of the buyer's address.
  • [new] Refund, chargeback, pause and end touch only this purchase's grants: source = statamic-payments and source_ref among the payment's and its subscription's provider ids
    (current, previous, first payment). Before, a refund revoked every grant of the buyer on that
    product, including a second purchase and one made by hand.
  • [new] A renewal extends this subscription's own grant instead of calling entitlements' renew(),
    which took any grant of the subject and could push a grant made by hand (or another
    subscription's) forward. Never shortens, fires EntitlementRenewed, grants only when the
    subscription has no grant of its own. Same for a pause in keep mode.
  • [new] A price change that restarts the agreement at the provider remembers the old provider id, so its
    grants and late cycles are still found.
  • [new] A subscription keeps billing and subject data of its first payment: an allow-list
    (entitlement_subject, team_id, team_uuid, paid_by, company, address,
    address_fields, vat_id), extendable with Subscriptions::inheritMeta(...). Cycles, follow-up
    offers and resumed checkouts keep the subject too. Agreements created before this release find
    the subject on their first payment.
  • [new] Team billing address for the invoice. A meta.address in the fields statamic-teams writes
    (company, name, line1, line2, postal_code, city, country; one of them is enough)
    becomes the text statamic-invoices prints; meta.company is filled from it when not set, the
    fields stay under meta.address_fields. An address given as text, or with fields of its own
    (street, …), is kept as it was.
  • [new] Control Panel: payments and subscriptions show "Bought for: Team …" in the listing and the
    detail; names are loaded once per page. The search finds a payment or subscription by the team's
    name. The payment detail shows the company when it differs from the name.

1.26.0

Choose a tag to compare

@goldnead goldnead released this 24 Sep 14:25

Upgrading

  • No migration, no new permission.
  • With statamic-webhook-manager 2.10 the payment and subscription moments appear there as
    triggers.
    Nothing to do if you want that. To switch it off, set
    statamic-payments.webhook_manager.enabled to false (env STATAMIC_PAYMENTS_WEBHOOK_MANAGER).
    Without the webhook manager nothing changes.

Added

  • [new] Webhook Manager triggers. With goldnead/statamic-webhook-manager installed, the twenty
    payment and subscription moments appear there as triggers under the automations handles
    (payments.paid … payments.subscription_ended), labelled in German and English. Each body is a
    chosen list of fields, never a model: no card data, no mandate, no provider responses, no
    thank-you or portal token, the IP only as its /24 or /48 network. Every body carries event,
    occurred_at, brand (id, handle), subject_type and subject_id; the full list per
    trigger is in the README. Delivered in the brand of the row, so a provider webhook or the
    reminder command reaches the right brand's hooks. SubscriptionCycleFailed and
    PaymentCommunicationLogged are not offered, as in automations.
  • [new] Every body carries event_id (sha1 of handle and what makes the moment unique: the object,
    its own time or reference), the same when the same moment is told twice, so receivers can drop
    duplicates. occurred_at and the manager's event time are the moment's own time (paid_at,
    refunded_at, ended_at …), not the time of sending.
  • [new] Handed over after the surrounding database transaction commits, never after a rollback (the
    dunning run ends a subscription inside one).
  • [new] A row naming a brand that cannot be set is not delivered at all (logged), instead of going out
    through the current brand's hooks.
  • [new] The README says it plainly: order between triggers is not guaranteed; sort by occurred_at,
    deduplicate by event_id.
  • [new] Config webhook_manager.enabled (STATAMIC_PAYMENTS_WEBHOOK_MANAGER, default on).
  • [new] The coupling is optional: composer suggest, the manager's classes are checked by name before
    anything that implements its interface is loaded, and registration retries at the end of the
    booted queue. A new test boots the addon in its own process with the manager and brand-context
    hidden from the autoloader.