Active and Void tabs in the platform org directory
The console's Organizations tab now splits into Active and Void sub-tabs. A voided organization is orgs.is_active = FALSE (set by SecurityService's void endpoin...
Rerun a webhook event whose first attempt died, and refuse one the SDK cannot read
A losing claim on an unprocessed row always answered 500, so an event whose first attempt failed (a unique-constraint race inside the invoice.created/finalized/...
Only a failed charge starts dunning, and Stripe must agree before dunning acts
invoice.finalized carries status=open with an amount due for every auto-charged invoice before the charge is attempted, and the invoice handler started the deli...
The same-commit-rebuild guard could never fire, and the digest check failed open
Both protections against a deploy that deploys nothing were inert, so the pipeline reported two green jobs while the pods stayed on the previous digest. That is...
A catalog row naming an app this build cannot resolve is not for sale
ServiceTypeConverter stopped throwing on an unknown app id and started resolving it to null, which is what a service must do β the enum ships inside each jar wh...
A price Stripe has never heard of is a fault, not a silence
The reconciler compared mapped prices against the catalogue and returned early whenever nothing was mapped at all. So a term the catalogue had gained since the ...
Charge what the screen says, and refuse rather than pretend
An audit of the Plans & Billing console against live Stripe keys found the seam between the screen and Stripe broken in every direction at once, and silent in a...
A plan's startup fee, and the live offer, on the wire
PlanDto carried only the recurring rate, so every surface quoting a plan quoted a price the customer would not be charged: KamoCRM's startup fee is $99 a seat, ...
One endpoint behind the customer's own plan page
GET /api/billing/self/plan answers everything /myplan renders: the plan, what the organization can actually use, the catalogue to compare against, seats and the...
Prove the deploy by digest, not by tag
The preceding commit stops `set image` being a silent no-op. This asserts the outcome: after the rollout, the tag is resolved to a digest at the registry and th...
A rebuild of the same commit deployed nothing and reported success
The image is tagged with the commit SHA, so rebuilding the same commit produces an identical image reference. `kubectl set image` then changes nothing, the Depl...
Bill the startup fee on both routes to a plan, and record negotiated terms
Two routes reach the same plan and only one of them was going to charge the new per-seat startup fee. A sales-written order bills through SubscriptionService, w...
Lock every money-moving sweep, and run two pods
BillingService ran one pod and its seven scheduled sweeps were correct only because of it. There is no ShedLock anywhere on this platform, so a second replica w...
Let a rollout finish what the old pod was doing
Deploys replaced the only pod of each service with nothing to catch the requests in flight. Three settings, applied across the fleet: - preStop sleeps 10s befo...
Stop offering the internal platform-owner plan for sale
listPlansForMarket filtered on isActive alone, and active is not the same as on the menu. OWNER_UNLIMITED ("Platform Owner β Internal") is active, carries no pr...
Serve the running offer, and say whether a seat is actually PAID for
Adds GET /markets/{id}/promotion. There was no way for an in-app buying surface to know an offer was running at all β the marketing site read promotions through...
Bill the three-year term, and quote its rate to the console
BillingCycles gains TRIENNIAL, expressed to Stripe as three YEARs rather than thirty-six MONTHs so the renewal tracks the signup anniversary β the same reason A...
Per-server lines on the extensions summary
The phone server's extensions tab showed the organization-wide figure and nothing else, so an empty KamoPBX server displayed "21 billable extensions β $63.00 / ...
The platform org is not gated by its own catalog
The platform org was told "Requires the KamoPBX Hosting add-on" on its own phone-server screen, for an add-on it publishes. It owns the plan catalog, the add-o...
Refuse a self-serve Enterprise subscription
Enterprise is negotiated. Hiding the price is presentation; this is the boundary. A request naming the Enterprise plan is refused unless the caller holds ******...
Bill phone service by the extension
PricingModel.PER_EXTENSION, its own reserved quantity key, an allocator, a nightly reconciler, a removal gate and the summary API - the phone twin of the per-ma...
Scan the shared-lib email.billing repository package
@EnableJpaRepositories lists packages explicitly, so MailboxBillingDayRepository was invisible and the service refused to start outright β caught by booting it,...
Rebuild against current kamo-shared-library
CI clones shared-lib at image build time. This service was last built before shared-lib gained GMAIL_PERSONAL and OUTLOOK_PERSONAL, so its deployed enum cannot ...
Bill e-mail per mailbox, reconciled nightly
Quantity is server-computed under a reserved key a request body cannot reach, and pushed through the existing amend path β so Stripe's CREATE_PRORATIONS charges...
Close the webhook idempotency gap BL0's final review found
Finding 1 (Critical): a losing claim on a redelivered Stripe event only proved a row existed, not that the prior attempt finished. A claimed row left behind by ...
Do not key ephemeral per-attempt Stripe creates
Round 2 review (Ruling 17) found "is this a create?" is necessary but not sufficient. The question that matters: can this subject legitimately be created more t...
Keys belong on Stripe creates only, not on mutations
Round 1 review (Ruling 15) found the Critical: a stable idempotency key on an update/cancel/attach/detach is not a safeguard, it is a bug. Those calls already s...
Make every Stripe write idempotent, and fail the build on a new one without a key
A socket timeout on a Stripe write that actually succeeded is indistinguishable, from our side, from one that did not; without an idempotency key the retry crea...
Commit the processed flag with the money write, not ahead of it
markProcessed was REQUIRES_NEW, so it committed processed=true while the handler's money writes were still uncommitted in the suspended outer transaction. JpaTr...
Prove library HQL parses on the Hibernate this service actually runs
kamo-shared-library builds on Boot 3.3.4 / Hibernate 6.5; BillingService runs Boot 3.1.5 / Hibernate 6.2.13. The library's own HQL tests therefore pass vacuousl...
Make a replayed Stripe event a no-op
Adds BillingWebhookEventClaimService on its own bean, with REQUIRES_NEW on every method, and wires it into processEvent ahead of the dispatch switch. A losing c...
Pin the optional signature header and cover all three failure branches for body leaks
Answer Stripe webhooks with the right status and never echo an exception
Pin kamo-shared-library 1.6.0
The library moved to 1.6.0 while every consumer stayed on 1.5.0. Consumer CI builds the library from a fresh clone into a cold ~/.m2 and ci-settings.xml mirrors...
Stop selling add-ons the chosen plan already includes
The catalog has always recorded which add-ons a plan bundles, in **************** β Business and Enterprise carry video conferencing, the AI assistant, the HR s...
Publish the paid conversion for tracked organizations
Both paths that stamp BillingSourceType.PAID now publish marketing.visitor.paid, because hooking only one silently misses half the traffic β AccountSubscription...
Size the pod above its own JVM heap ceiling [skip ci]
The image starts the JVM with -XX:MaxRAMPercentage=70 -XX:+AlwaysPreTouch, so the heap alone may take 70% of the container limit and pre-touch keeps every commi...
Decide platform access with the shared resolver
@PlatformAdminOnly now names the platform right it requires (defaulting to MANAGE_ORGANIZATIONS, which is what this service's platform surface is about) and the...
Carry the renewal date on the coverage and group payloads
Both surfaces now report currentPeriodEnd and cancelAtPeriodEnd. The date is what a cancel confirmation actually needs to say β "you keep your seat until the en...
Cancelling stops future billing and nothing else
Cancel no longer takes an "immediate" flag, and no customer-facing route can ask for one. Passing it true ended the subscription the same day while asking Strip...
One plan per organization, a 25-seat Enterprise ceiling, and a way to stop paying
The plan is now an organization-level decision and nothing else can override it. Letting a self-payer or a group manager pick their own reads like a kindness an...
Let a group's plan be cancelled, and stop double-paying for one person
Three gaps in what shipped last: A group with a plan could never be removed. delete() refuses while a live subscription exists β correct, since the group row i...
Make billing groups work end to end, and stop self-pay charging for nothing
A group can now gather people, buy a plan on its manager's own card, and hand its seats out. None of that existed: the only operations were create, approve and ...
Rebuild against the renamed pay-group table
The entity moved from billing_groups to billing_pay_groups after it turned out the former is a live legacy table with a different shape. This service still hold...
Stop shipping a shared credential as a config default
The platform's shared password was baked in as the fallback for DB_PASSWORD and MINIO_ROOT_PASSWORD, so it lived in this repo and in the built image as well as ...
Tell people their payment failed, and stand the subscription down
The dunning that did not exist. invoice.payment_failed wrote a row and returned: no email, no flag, no state change, and billing.email-service-url was configure...
Let a member buy their own seat, and close a body-parameter hole
Self-pay had no route inside the console. Everything on the billing surface is account-shaped β you must already hold an AccountMember row before Plans & Billin...
Org policy and billing group APIs
Who inside an organization may pay Kamo is now answerable and settable. Both routes are org-scoped and therefore deliberately outside /api/billing/accounts/** a...
Like what you see shipping?
Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.