Mabadiliko
Nini kipya.
Kila kuboresha jukwaa, kama ni meli.
· kamo-internal
Author scripts in CRM settings, read them on a call
Two CRM settings tabs behind MANAGE_SALES_SCRIPTS — the live scripts and the archive — plus a Sales Scripts tab in the Interaction Center behind VIEW_SALES_SCRIPTS, and a pin that keeps the Interaction Center open. THE READER IS A TELEPROMPTER, NOT A DOCUMENT. The only question that matters while somebody is talking to a customer is "where am I", so a progress spine runs down the block stack and fills as parts are ticked, the next unread part is lifted to full contrast while read ones recede, and ticking one scrolls the next into place. A script genuinely is a sequence read top to bottom once, which is what earns the sequential treatment. Everything else on the surface is kept quiet so that one thing can carry it. The composer mirrors the same spine, so an author is looking at the shape the agent will see rather than at a form that produces it. Read-progress and the pin both live in sessionStorage: they survive a reload in the tab the member is working in, do not follow them into a second tab handling a different call, and are gone at sign-out — app/logout already does a wholesale sessionStorage.clear(), so neither needed unwinding. Every accessor is wrapped, because sessionStorage THROWS rather than returning null in a hardened profile, and this code runs inside app-wide chrome. NavTop's opener gate widens from chat access alone to chat OR sales scripts. It had to: a CRM agent on an org without the Chat app could not reach the Interaction Center at all, so the Sales Scripts tab would have been invisible to exactly the people it is for. Each tab inside still carries its own gate. The composer takes its "nothing changed yet" baseline one macrotask after loading rather than from the values it seeded. Lexical re-serializes a document when it loads it — equivalent JSON, rarely identical — so comparing against the server's string reported every freshly opened script as edited and prompted "leave without saving?" on the way out of one nobody had touched. Blocks with neither a heading nor any text are dropped on save. An author who added one and moved on did not mean to publish an empty step, and the reader would render a checkbox against nothing. The two new landing cards join hasActiveMarkets and canManageSalesScripts to the card memo's dependencies. Both settle asynchronously, so without them the memo was built while both were false and never rebuilt. The four market-gated CRM cards above were already relying on some other dependency changing in time; naming hasActiveMarkets fixes them in passing.
· KBService
Serve the scripts an agent reads on a call
/api/kb/sales-scripts, gated on the two new rights. Reads take VIEW_SALES_SCRIPTS or MANAGE_SALES_SCRIPTS, writes take MANAGE. SessionHelper.hasRight fails closed, which is what every gate in this service depends on. Here rather than in SecurityService because APIService already forwards /api/kb/** to this service wholesale, so the feature needs no gateway change, and because donotmodify.dao.repos is in this application's @EnableJpaRepositories — the lead-market lookup a script needs is a local query rather than a cross-service call. The list endpoint returns summaries with a block COUNT and no block bodies. A settings list of a dozen scripts has no use for a dozen full Lexical documents, and the counts come from one grouped query rather than one per row — the shape that made /leads slow enough to be reported as an outage. GET /markets exists so the Interaction Center can draw its sub-tabs in one call. The alternative was fetching every market and every script and intersecting them in the browser: two round trips and a payload of script bodies to answer a question about ids.
· kamo-shared-library
The script a member reads on a call, as ordered blocks
Two tables and two rights. SALES_SCRIPTS hangs off a lead market the way a LeadVendor does; SALES_SCRIPT_BLOCKS holds the parts, each one a Lexical document plus the flattened text the previews and reading-time estimate read. Blocks are rows rather than a JSON array on the script because a block's id is what a member's read-progress is keyed on. As an array, position would be the only identity available, so an author inserting a paragraph near the top would silently shift everyone's place in the script. With stable ids the wording can be rewritten and the blocks reordered while every agent mid-call keeps the ticks they already made — which is why **************** preserves the id of any block the payload still names, and why the test for it is the first one in the file. Saving is a FULL REPLACE: blocks carrying an id update in place, blocks without one are created, and blocks the payload omits are DELETED. Stated on the DTO and covered by a test, because a caller assuming otherwise loses data with nothing to show for it — the same contract, and the same trap, as backgrounds/commit. Omitting `blocks` entirely is the separate, safe case that lets a rename leave them alone. VIEW_SALES_SCRIPTS (300) is a root: reading the pitch is not implied by any other right, least of all VIEW_LEADS, since the trainee who learns the script before being handed a lead is exactly who the grant is for. MANAGE (301) hangs off it so authoring cannot be granted without the ability to read what is being authored, and revoking the read takes the write with it. Both name ServiceType.CRM, which is what puts them in the CRM security group — permissionGroupTree derives groups from serviceType, so no group edit is needed and a right in no group could not be granted at all.
· DocsService
Fill a rectangle, honour a highlight's opacity, and redact
Three things about the way a prepared document renders the designer's marks. A rectangle was drawn as an outline, so the fill a member now picks to cover something up did not arrive. It fills first and strokes after, and only if there is a border left to draw — the opacity belongs to the fill, not to the border around it. A highlight ignored the opacity the designer has always saved and painted every one at 30%. And a rectangle marked "remove the text underneath" now has its text taken out by PdfRedactor rather than merely covered. That runs before anything is drawn, because it rewrites the page's own content stream. The annotations also move ahead of the page operations, which is a fix in its own right: an annotation names its page by index in the DESIGN, and applyPageOperations deletes pages, so every index past a deleted one shifted. A mark drawn afterwards landed on whatever page had moved up into that slot — which for a redaction box would mean clearing text on the wrong page and leaving the right one alone.
· ESigService
Put the designer's annotations into the signed document
flatten() applied blank pages, rotations, modifiers, field values, page order and certificates. It never drew the annotations. A rectangle, a highlight or an arrow drawn over a template showed up in the designer and in the signer's browser and then was simply not in the PDF that got delivered. For a highlight that is a missing flourish. For the rectangle a member draws over a bank account number it is the opposite of what they asked for, so all five types are drawn now, after the field values and before the watermarks that sit above — the order the designer stacks its layers in. A rectangle marked "remove the text underneath" also has its text taken out, by PdfRedactor, before anything at all is drawn: redaction rewrites the page's own content stream, so a mark stamped first would be rewritten away with the text it was meant to survive. Neither pass throws — a redaction that fails must not lose a signature — and a failure is logged for what it is, which is that the covered words are still in the delivered file.
· kamo-signer-monorepo
Draw a rectangle's fill in the signer's view
The designer can now give a rectangle a fill and an opacity, which is how a member covers something up. This renderer drew fill="none", so the signer saw an empty outline where a solid block was meant to be — and on a redacting rectangle they would have seen straight through to what was being taken out. The fill carries the opacity and the border stays solid: fading a border along with a 30% block makes it read as a mistake rather than a choice. A rectangle with no fillColor is the outline every one saved before this still draws.
· kamo-internal
Make the rectangle tool something you can hide things with
The rectangle tool has been in the annotation toolbar all along, but it drew an outline: fill="none" in every renderer. There was nothing to hide anything with, no opacity, and no way to touch a shape again once it was drawn — annotation selection was unreachable code, because the layer only took pointer events while a drawing tool was active and the click handler only ran while the select tool was. So a rectangle now has a fill, an opacity, a border that can be nothing, and a switch that says the text underneath is to be removed rather than covered (the backend does that part). It arrives as a solid black block, because covering something up is what people reach for it to do; the outline every rectangle saved before this still has is what no fillColor means, so none of them change. Drawing it is either gesture: drag it out, or click once to anchor it and again to finish — a press that goes nowhere used to be discarded as too small to keep, which is exactly the gesture that now starts one. Escape abandons it. Once drawn it is selected, with eight handles and a draggable body, using the same clamping and page-space mapping as a field's corner handles so the two feel alike. And because a selected annotation had nowhere to go, an empty properties panel used to slide up; it now has one, with the same controls after the fact. Two-point tools all take the second gesture, not just the rectangle, and highlights get the handles too — they are the same box underneath.
· kamo-shared-library
PdfRedactor, so a covered word is actually gone
A filled rectangle drawn over an account number hides it from the eye and from nothing else: the glyphs stay in the page's content stream, and a copy-paste or a text extractor hands them straight back. The esign designer is about to offer "remove the text underneath" on its rectangle tool, and this is what has to be true for that to mean anything. Every glyph a page paints is measured, and the ones a region covers are dropped from the token stream and replaced by a TJ advance of exactly the width they occupied — so the rest of the line does not move and everything else on the page is written back untouched. Text painted from inside a form XObject cannot be reached that way, because the page's own token stream does not contain it. So the work is verified afterwards by measuring the glyphs again, and a page that still paints text inside a region is re-rendered as an image. That page loses its searchable text, which is a poor outcome and is logged as one — but it is the only one of the two options that is not a silent leak. PDFBox is declared optional so it does not reach the twenty-odd services that never open a PDF; the two that do already declare 3.0.3 themselves.
· kamo-internal
Link the articles a draft names
The authoring convention is to name a sibling article by putting its title in italics — "Read *Security Roles Explained* before building them". 181 spans across the drafts do it and nothing ever turned them into links, so a published article tells the reader which one to go and read and then leaves them to find it. There is exactly one Markdown link in three hundred articles and it points off-site. It also fed the KB-link bug the assistant just had. contentPlainText is what gets embedded and what the assistant is shown, and a stripped italic reads as prose, so the setup checklist reached the model naming a dozen articles with no way to reach any of them. That is guarded in the AI service now, but a reference that is a link needs no guarding. Resolution happens at import, against the knowledge base being published into, because the address is a guid and a guid belongs to one knowledge base rather than to the drafts — writing them into the content files would tie the help center to a single organization. It runs on the converted tree, where a reference is already one italic text node holding exactly the title, which keeps markdown-to-lexical.mjs untouched so the other three hundred articles cannot regress. Only an exact title match is linked, and italics do double duty here — 62 italic spans are UI labels, emphasis or question titles and stay as they are. An article naming itself is not a reference, an author's own link always wins, and a second run is a no-op. Verified against the live knowledge base: 181 references, 119 distinct destinations, 110 articles gaining links, nothing unresolved.
· kamo-internal
A setup fee on a single-price quote, and the suite on its own line
The setup fee existed only on multi-option quotes, so 'One price' had no box for one — on the form or on the printed page. It now sits beside Shipping, appears in the live rail and prints as its own totals line. It is outside the discountable subtotal, mirroring QuoteTotals: a percentage off the goods is not a percentage off the cost of installing them. An address arrived as one comma-joined string, so 'Suite 400' ran into the street and the city. The unit is its own field now and gets its own line, under the street — on the quote's addressee block, on the seller's own letterhead details, and on the lead's Mailing Address card in view mode, which is where the same address is read before it ever reaches a quote.
· InitializerService
Commerce_quotes.setup_fee
A one-off setup fee on a quote that presents a single price. The fee already existed on QuoteOption, where each alternative carries its own; a flat sheet had no option to hang one from, so it could not record a setup fee at all. Nullable, so the ADD COLUMN is accepted on a populated table and every existing quote keeps meaning what it meant: no column, no fee.
· kamo-shared-library
A setup fee on a single-price quote, and the suite on its own line
Two things a quote could not state. A one-off setup fee lived only on QuoteOption, where each alternative carries its own. A flat sheet has no option to hang one from, so it could not record a setup fee at all — quoting one meant switching the whole quote to Compare Options, which changes how the document reads for the customer. Quote.setup_fee is the flat-sheet equivalent. It sits OUTSIDE the discountable subtotal deliberately: a percentage off the goods is not a percentage off the cost of installing them. An address was assembled into one comma-joined string, so 'Suite 400' ran into the street and the city. That is not how an address is written, and on a quote it is what a customer is expected to post a cheque to. The unit now travels as its own field on both the customer block and the org's own details, leaving the page free to give it its own line. The column is nullable — every row written before it has no fee, and null means none — so no existing quote changes value.
· kamo-signer-monorepo
The suite on its own line, and a setup fee on a single-price quote
Two things the printed page could not say. An address arrived as one comma-joined string, so 'Suite 400' ran into the street and the city — not how an address is written, and this is the page a customer is expected to post a cheque against. The unit now travels as its own field and prints on its own line, on the addressee block and on the seller's own details. The setup fee existed only on multi-option quotes, so a single-price quote could not state one at all; switching modes to record it changed how the whole document reads. It now has its own totals line, shown only when there is one.
· InitializerService
Reprice stored quote totals through the pricing engine
Quote money columns are written, not computed on read, so the two corrections that landed on 2026-09-02 — a unit price is a per-month rate the term multiplies, and a committed term is worth what it commits to — leave every existing row carrying the old arithmetic until something saves it. On the quote that exposed this, a three-year option stored one month of itself. Calls QuoteService.reprice, the same code that prices a quote on save, rather than reimplementing the money rules in SQL. A backfill with its own copy of them is a second source of truth and the two drift the moment either is touched. Terminal quotes are reported, never rewritten. An ACCEPTED quote's total records what a customer agreed to at the figure they were shown; that figure may be wrong, but correcting it here would silently restate a signed document. Each is logged with its stored and recomputed value for a person to decide. Idempotent, and ordered late because it loads entities.
· kamo-signer-monorepo
Print the full line on the customer's quote
Each line showed a name and an amount. That asks the customer to take the quantity, the unit, the rate and any reduction on trust, and leaves them nothing to check a later invoice against. Option cards now print the same columns the flat sheet does, and both gain a Discount column: a reduction that was agreed belongs on the document that states the agreement. Lines with no discount show a muted dash rather than a gap, so a column of them still reads as deliberate. Matches the change to kamo-internal's print preview, which exists to project this page — the two are only useful to staff while they agree.
· kamo-internal
Print the full line on the quote, and stop losing work at the mode switch
Two things a quote could not do. The printed line items inside an option card showed a name and an amount, nothing else — no quantity, no unit label, no rate, no discount. That asks the customer to take all four on trust and leaves them nothing to check an invoice against. Option cards now print the same columns the flat sheet does, and BOTH gain a discount column: a reduction that was agreed has to be visible on the document that states the agreement. Switching between 'Single price' and 'Compare options' carried work one way only, and only when no option existed yet. Flipping across and back therefore discarded whatever had been typed in whichever view was left behind. The two modes are two views of the same first thing being sold, so the switch now carries it both ways; options past the first are alternatives and are left alone, since the flat sheet has nowhere to hold them. The carry is two exported pure functions rather than logic inside a click handler, so the behaviour that was silently wrong is now the behaviour under test.
· kamo-internal
Re-issue a share link from its row, and stop losing the one-time URL
The Share links list offered no way to view or copy a link, and nothing explained why. Nothing can reveal an existing one — only the token's SHA-256 is stored, so the URL exists in the mint response and the customer's inbox and nowhere else. The architecture's own answer to a lost link is to mint another, which leaves the copy already sent working and independently revocable; that answer just was not reachable from the UI. Each row now carries it: Copy a new link mints silently and opens the copy dialog, and where the row names a recipient, Email a new link sends it too. Both go through one doMint path with the dialog's own create, so the three cannot drift. A caption under the list states the constraint instead of leaving the reader hunting for a button that cannot exist. Two smaller repairs to the same complaint. The one-time URL dialog was dismissable by backdrop click and Escape — one stray click and the link was unrecoverable; it now closes only through Done. And the mint response's `emailed` is the OUTCOME while the request is the INTENT: asking for an email and not getting one rendered the same "Link created." as never asking. The two are tracked separately now and a failed send says so.
· RAGService
Resolve the articles a passage names into link targets
A hub article — "Setting Up Your Organization — A Checklist" is one — exists to send the reader to a dozen others, and names every one of them in plain text. The editor stores no link nodes, so the titles are all that reach contentPlainText, which is what gets embedded and what the assistant is shown. Search then returned that passage with the paths of only the handful of articles that matched the vector query, and the assistant, told to link what it cites, put one of those paths on all twelve recommendations. The search response now carries linkableArticles: the articles those passages name, resolved to real guids, filtered by the same audience tiers the search filter uses — an article the assistant may quote is one it may link. A title is only matched when it is specific enough to mean one article. "Chat", "Leads" and "Notes" are article titles here too and appear in ordinary prose constantly, so matching is limited to titles of three words and twelve characters, on whole-word boundaries. The catalogue is titles and flags only, never the @Lob bodies, and is cached per org — this runs on the search path.
· kamo-internal
Ask whether the guests should be told, and how
Adding a guest to an event did not tell them, and the dialog never said so. The only thing that ever wrote to a guest was a reminder — which reaches a colleague in-app and reaches an outside address never — so inviting a client to a meeting contacted nobody, and there was no control anywhere that would have. A card under the guest list, in the shape of the privacy and reminder cards it sits between: one switch, and two route chips under it. The switch turns itself on when the first guest arrives, once and never again, because adding somebody to a meeting and having them not be told is the surprising outcome — and turning it back off has to stick even while more guests are still being added. The summary line is the part that earns the card. An outside guest has no chat thread and no Kamo calendar; email is the only route that reaches them and it is what carries the calendar file. Switching email off strands them, and nothing else in the interface would say so. Turning off the last route turns the whole thing off rather than leaving a switch that claims to be inviting people while sending nothing. Hidden for the lead callback, which owns its own save into SecurityService: mailing a lead a meeting invitation for the moment you intend to ring them is not what scheduling a callback means. "Update and notify" goes. It posted to the mail composer's own invite endpoint, which mints its own event and refuses any whose ORGANIZER_EMAIL is not the sender — a column no calendar event has ever set — and sent the guest list as it was when the dialog opened, so an edit that added somebody notified everybody except them. It could never have run in any case: it was gated on attendee.isOrganizer, which the calendar's guest sync writes as false for every row. Saving now does the job.
· InitializerService
The columns that let an event invite its guests
Three on calendar_events, because whether a meeting invites people belongs to the meeting rather than to the moment it was saved — an edit six weeks later that adds a guest has to know. All default to leaving existing events silent: CalDAV, the ics importer and the lead-callback composer all write through the same service and none of them offers this choice. Two on calendar_event_attendees, both nullable, and null is the load-bearing value: it means "never told", which is what every existing row is and what every guest added by a later edit will be. A default on either would make the whole feature silently do nothing. Applied by hand to the cluster, as KamoInitializerService is not deployed.
· EmailService
Send the meeting invitation the guest list implied
Adding somebody to an event's guest list did not tell them. Only a reminder ever wrote to a guest, and a reminder reaches a member in-app and an outside address never — so a client invited to a meeting was never contacted at all, and a colleague first heard about it fifteen minutes before it started. EventInviteService runs after the save commits, on an async thread. After the commit because an invitation for a meeting whose save then rolled back cannot be recalled; off the thread because afterCommit still runs before the response is written, and fifty SMTP conversations there is fifty seconds of spinner on a save that already succeeded. Two routes, and the difference between them is why it is two controls. Email is the only thing that can reach an address belonging to nobody here, and it carries the ics attachment — which is the entire mechanism by which a meeting arranged in Kamo lands on an outside guest's own calendar. Chat reaches a colleague in the thread they already have with the organiser, through the same MediaService pair AlertChatService uses, and skips an external guest by construction: a chat thread is between two members, so there is nobody to address. Who is owed what comes from the guest row rather than from what the save inserted. Never told means an invitation, whenever they were added. Behind on SEQUENCE means an update. Level means silence, so correcting a typo does not mail the room. updateEvent raises SEQUENCE only when the meeting itself moved — the description is deliberately out, because it is where agendas go and mailing every guest per line would train the room to ignore the mail that says the time changed. The record is written after a route succeeds, never before: marking optimistically would turn one transient SMTP failure into a guest permanently believed to have been told, which is the silence this exists to end. Also sets ORGANIZER_EMAIL, from the mailbox actually being sent from. No event made through the calendar has ever carried one, and an ics without it cannot be replied to — the guest's "accept" has no address to go to. Deleting an event that invited people now retracts it from the guests who were told, and only those.
· kamo-shared-library
Let an event invite its guests, and put it on their calendar
Saving an event told nobody. The guest list was materialized and the audience stored, and the people on it heard nothing until a reminder fired — which reaches a member in-app and reaches an outside address never. So a member could invite a client to a meeting, watch the name appear on the guest list, and the client would simply never be told. Nor could a guest see the meeting. Visibility was decided by the owning calendar alone, so an event on the organiser's calendar was invisible to everyone invited to it. **************** has had a switch in Calendar Preferences and a column behind it since that screen shipped, and nothing has ever read it. The event carries the choice — whether to invite, and by email, chat, or both — because an edit six weeks later that adds a guest has to know whether this is a meeting that invites people. The guest row carries when it was last told and at which SEQUENCE, which is what makes "a guest added later gets an invitation to the same meeting" correct across any number of edits: deriving it from whichever rows a save inserted is right exactly once. Visibility gains an invited-guest branch through an explicit LEFT JOIN, gated on that preference via a member id nothing can match. One row rather than a copy per guest: one RSVP, and an organiser moving the meeting moves it for everybody. The LEFT JOINs are load-bearing — the organisation calendar has no owning member, so an implicit path expression drops every shared-company event, the trap findBusyForMember already documents.
· SecurityService
A member address signs you into its own org, not every org
A member who signs in with the org mailbox flagged Primary on their member settings was being offered every workspace their user account belongs to. The address exists only inside one organization — mailboxes are unique on (org_id, email), not globally — so signing in with it is signing in THERE. The account's other workspaces stay reachable by the account's own username or e-mail, which is what an account-wide identifier is for. The identifier now carries a scope. An account identifier leaves it empty and nothing changes; a member mailbox address names the org(s) it is Primary in, and that set is the only thing the sign-in may enter. Narrowing the list the picker draws is cosmetic on its own: the choice comes back from a browser, and the member usually IS a genuine member of the other workspaces, so /session/select's membership re-proof would pass. The restriction therefore lives in the selection token, in Redis, where the caller cannot reach it — and goes back with the token when a transient fault returns it, or a retry would silently widen into an unrestricted sign-in. - PrimaryMailboxIdentityLookup: the unscoped lookup returns the org ids with the user id. Still refuses when the address names two different accounts; one account holding it in two of its own orgs offers both, and nothing else. - CredentialResult gains orgScope, empty meaning unrestricted. - loginWithoutAResolvedHost filters enterableOrgs by it (only ever removing, so an inactive membership still falls out), refuses an ?org= hint outside it in the words a wrong password gets, and no longer preselects a last_org_id the address cannot reach. - OrgSelectTokenService stores userId:orgIds; a bare user id from a pod mid rolling deploy still decodes, as unrestricted. An unreadable restriction fails closed. - SessionSelectController refuses an out-of-scope org 403 and does not hand the token back — that is not a fault the member did not cause. The custom-domain path is untouched: the host already names one org.
· SecurityService
Enforce the platform's visibility, and let it be edited
GET/PUT /api/security/reports/catalog, both gated on MANAGE_REPORTS on the platform spine. The list comes from the REGISTRY, not the override table: an operator has to be able to restrict a report that has no row yet, which is every report until somebody touches one. The check itself went into ReportScopeResolver.mayRun, which the catalogue, the runner, the pickers, the exporter, the saved-view list and the schedule sweep all already cross. Adding it to each of those instead would have been six chances for one of them to disagree — and the one that disagreed would be the one that showed a report the platform had hidden. It sits IN FRONT of the report's own rights, and god does not pass it. Break-glass widens what a member may see within their organization; this is a question about which organizations a report exists for at all, and a god in a tenant is still in that tenant. ReportVisibilityService fails OPEN, which is the opposite of everything else here and deliberate: an unreadable override table would otherwise make every report on the platform vanish, and the reports underneath are still gated by their own rights and their own row-level scope. Losing this table costs a restriction, not a disclosure. Setting ALL_ORGS deletes the row rather than storing the default. A restricted report stops its existing schedules, and the run is recorded FAILED with "the schedule's owner no longer has access" — the same audit trail as revoking somebody's right, which is the same thing happening.
· kamo-internal
Decide who sees each report, from Platform Administration
A Reports tab on the Configuration console, gated by MANAGE_REPORTS on the platform spine — which is also, with no further work, a row in the Manage Rights dialog, since that dialog renders the server's catalogue. Every report the platform ships, grouped by folder, each with a three-way control: all orgs, the platform organization only, or platform administrators only. Each row also shows the right a member still needs, because an operator restricting a report should be able to see it is already gated — most need nothing here, and a console that hid that would invite restricting things twice. Saves per row, optimistically. A three-way control that waits on a round trip feels broken, and the only cost of being wrong is the button snapping back with the reason. The copy is careful about two gates that sound alike. This is the outer one: inside an organization a report still needs the right governing its data, and people still see only the records they are allowed to. The help says out loud that restricting a report stops any schedule already set up for it — correct behaviour, and the least discoverable consequence on the screen. Two things the type system and a guard caught rather than review: RIGHT_ICON is an exhaustive Record, so the new right could not ship without a glyph; and the tab had no Platform Administration menu link. Every other tab has one, so the guard now requires it — a tab reachable only by somebody who already knows the tab strip is there is a tab nobody finds.
· kamo-internal
Give the quote billing choice a screen to live on
The org-wide setting deciding whether a quote is billed each period or in full up front has existed on OrgPOSConfig for some time, and the endpoint that writes it has too. The only component rendering it — **************** — is imported by nothing, so no member has ever been able to reach it: the setting was configurable only by hand-posting to the API. Commerce settings gains a Quote Billing tab. The panel carries only the billing choice; that orphaned component's other half, the engagement-type toggles, is already served by Markets & Fields on this same page. The copy is read from its message namespace rather than duplicated, since it is already translated. Saving sends the engagement types back exactly as read — the endpoint treats the enabled list as the whole truth, so omitting it would disable every vertical the org has.
· kamo-shared-library
Let the platform decide who may see each report
MANAGE_REPORTS, appended to PlatformRightType, and a REPORT_CATALOG override row per report holding one of ALL_ORGS, PLATFORM_ORG_ONLY or PLATFORM_ADMIN_ONLY. This is a coarser gate than everything else in the framework and it sits in front of them: a report's own requiredRight decides what a member sees INSIDE a tenant, and row-level scope narrows further. This decides whether the tenant is offered the report at all, which is the platform's decision rather than the organization's. On the platform spine deliberately. A tenant administrator can edit their own security models, so a RoleRightType here would let a customer widen a report the platform had restricted. The three settings are INDEPENDENT conditions, not a ladder — a platform admin inside a tenant satisfies PLATFORM_ADMIN_ONLY and not PLATFORM_ORG_ONLY, and a member of the platform org who is not an administrator satisfies the reverse. Reading them as a ranking would quietly widen one of the two, and the widening would look like the feature working. A report with no row is ALL_ORGS, which is what every report does today — so this table starting empty changes nothing live, and restricting a report stays a deliberate act. Appended at the end of the right enum because getSortOrder() is ordinal(): an insertion would renumber the console. Grants are stored by name, so appending changes nothing already granted.
· kamo-signer-monorepo
Show the whole term when the seller bills up front
The card led with the recurring rate regardless, so a customer signing a three-year deal payable at signing saw a monthly figure and a separate total below it. Where the quote says the term is due up front, that total is now the price on the card and the sub-lines that restate it are dropped. Reads a missing basis as per-period, so nothing changes until the API ships the field.
· kamo-internal
Lead with the whole term when the bill is due up front
An org that charges the committed term at signing saw the monthly rate on its own three-year quotes. The quote now carries the basis it was raised under, and the card, the live rail and the print preview each lead with the figure that basis actually asks for — the whole term, labelled 'due up front', or one period as before. The choice is one exported helper, headlineAmount, rather than the same ternary in three JSX blocks: these surfaces must not be able to disagree about which number is the price. The monthly equivalent stays beside it either way, because it is the only figure on which a three-year and an annual option compare. Reads undefined as the per-period default, so this renders exactly as it does today until SecurityService ships the field.
· InitializerService
Commerce_quotes.billing_basis
The per-quote snapshot of the org's quoteConversionBilling choice, so a quote keeps the terms it was raised under after an administrator changes the setting. Nullable — every existing row predates it, and null resolves to FIRST_PERIOD_PLUS_SETUP, the behaviour those quotes were sent under. @Order(0) for the usual reason: once shared-lib maps the column, any runner that loads Quote emits it in its SELECT, and a late-ordered migration never runs.

Ulimwengu wako unasubiri.
Kuanza bure katika dakika. Ongeza bidhaa wakati wowote wewe ni tayari - hakuna uhamiaji, hakuna logins mpya.
Bure milele - hakuna kadi ya mkopo inahitajika