Nenda kwenye maudhui
KAMOUniverse

Mabadiliko

Nini kipya.

Kila kuboresha jukwaa, kama ni meli.

  1. · kamo-shared-library

    Page and filter an account's quotes in the database

    An account view needs every quote across all its leads. Loading them to render a page degrades every month the account is in use, and any total shown would only cover what was fetched — so paging, filtering and sorting all happen in SQL. The query is native because Quote holds only a soft leadId with no association to join through; it joins leads.uid = commerce_quotes.lead_id and scopes on leads.account_uid. Pulling the account's leads into an IN-list would not survive a large book of business. Every filter is a null-guarded predicate, so one query serves the unfiltered grid and any combination of filters rather than a method per permutation. Sort keys are allow-listed: the value reaches a native ORDER BY, so accepting an arbitrary field name from the client would be an injection point. The list projection also gains sentAt, acceptedAt, version and the option/line counts — an overview is read for "how long has this been out, did it land", which the previous fields could not answer.

  2. · kamo-signer-monorepo

    Show the assigned agent on the customer's copy

    Inalingana na muhtasari wa wafanyakazi: kichwa cha barua kinataja org, hii jina mtu kuuliza.

  3. · kamo-internal

    Show the assigned agent on the preview

    Jopo la "Mawasiliano Yako" juu ya footer - jina, cheo, simu na barua pepe - hivyo mteja Kujua nani kuuliza kuhusu gharama. Kukataa kabisa wakati uongozi haukubaliwa.

  4. · SecurityService

    Resolve the agent alongside the customer, from one lead lookup

    Both blocks come off the same lead, so the controllers fetch it once and derive each from it rather than hitting the service twice per render. Still org-scoped in both places; on the public page the scope comes from the share link's own organization.

  5. · kamo-shared-library

    Name the lead's assigned agent on the document

    Kichwa cha barua hutambua org, lakini mteja aliye na swali kuhusu mahitaji ya bei mtu ambaye ana uhusiano. Orodha hiyo sasa inashikiliwa na maafisa wa usalama – Jina, jina la kazi, barua pepe ya kazi na simu ya kazi. Maelezo ya mawasiliano ya kazi tu: quote hubeba utambulisho wa biashara wa wakala, kamwe Idadi ya watu ambao wanaishi kwenye rekodi ya wanachama wao. Jina la kazi linapendekezwa JobTitle Official, ambayo ni nini org aliamua kuchapisha, kabla ya kuhusishwa JobTitle. Kiongozi asiye na saini, au wakala asiye na chochote kilichorekodiwa, haitoi kizuizi badala ya "Mawasiliano yako" juu ya nafasi tupu. Jina la azimio reuses ***** njia hiyo LeadController tayari anatumia kwa ajili ya afisa wa mkopo aliyepangiwa, hivyo quote majina wakala hasa kama Maoni yote ya msingi hufanya.

  6. · kamo-internal

    Reassign the agent holding an opportunity

    The approver's dialog gains an agent picker, drawn from the same pool the backend validates against. The grid's Agent column now shows who HOLDS the opportunity, with an amber "Reassigned from …" caption when that is no longer the agent who asked — the requester stays on the record and in the history either way. The lead-header control follows the holder too. It was keying off the requester, which after a reassignment would have offered the extension dialog to the agent who no longer owns it and hidden it from the one who does — and the backend gates that endpoint on the holder, so the offer would have ended in a 403.

  7. · SecurityService

    Reassign the holding agent, and email everyone it affects

    An approver can now hand an opportunity to a different agent. The requester is never rewritten — applyPrimaryAgent records the move as its own history row with both names, so six months later the record shows that it moved rather than reading as though the new agent asked for it. Extension ownership, the lead-header control and the detail mask all follow the holder now; the "Agent" filter matches EITHER end of a reassignment, because hiding it from the agent who found it and from the agent who holds it are both wrong answers. Nine notifications cover the lifecycle. Requests and extension requests go to everyone who can decide them — both decision rights, since a member who can only deny would otherwise never learn a request exists to deny. Decisions and edits go to the holder, the original requester when that differs, and every colleague on a commission split; an edit additionally mails whoever it displaced, because losing a commission share silently is exactly what these emails are for. Two things are load-bearing in how the send is sequenced: - Resolution and rendering happen EAGERLY, inside the caller's transaction, while the entity still holds the values the email is about. approveExtension() clears the extension fields immediately after, so a later read would describe "no additional time" on an email whose subject is how much time was granted. - The SENDING waits for **************** Announcing a decision that a later failure rolls back would leave agents holding an email for an approval that never happened, and afterCompletion rather than afterCommit means the database connection is released before the outbound HTTP starts. Detail-table values are escaped at the point they enter the map: member names are org-entered free text, the table is injected as raw HTML, and the renderer escapes nothing. An edit that changes nothing sends no mail, and a PENDING reservation sends no "updated" — nobody has been told it exists yet. 60 tests.

  8. · EmailService

    Preview samples for the nine opportunity notifications

    Without an arm here the admin Email Templates editor renders every placeholder blank, so anyone customising these nine would be editing an empty skeleton. One arm for all nine: they share a variable set by design, so a single sample map renders every one of them faithfully. Map.ofEntries because Map.of caps at ten pairs and the set is larger than that.

  9. · InitializerService

    Reassignable primary agent on a reserved opportunity

    PRIMARY_AGENT_MEMBER_ID is who holds the opportunity now; REQUESTED_BY_MEMBER_ID is left untouched forever. Nullable with no backfill on purpose — null means "never reassigned", and copying the requester into existing rows would destroy that distinction on every reservation that predates the column. Applied against production on 2026-08-04; column and index verified present.

  10. · kamo-shared-library

    Reassignable primary agent, and nine notification templates

    An approver can hand an opportunity — and the commission with it — to a different agent. PRIMARY_AGENT_MEMBER_ID is who holds it now; REQUESTED_BY_MEMBER_ID is never written again, because who found the deal is a fact about the past and overwriting it would erase the only record that the original agent brought it in. Null means "never reassigned", which is a different statement from "reassigned back to the requester" — so existing rows are left alone rather than backfilled. effectiveAgentMemberId() is the holder, and it is what now decides who may ask for an extension and who the lead header treats as the owner. Once an opportunity moves, the original requester is a name in the history, not a claim on it. Nine canonical templates cover the lifecycle: a request or extension request goes to everyone who can decide it; every approval, denial, edit, extension ruling, expiry and close goes to the agent holding it, the agent who asked if that is now someone else, and every colleague on a commission split. They share one generated shell so the layout cannot drift between them, and one variable set so a template can reference anything without a matching Java change — which matters because a missing variable renders as an empty string with only a WARN, and ships an email reading "reserved until ".

  11. · kamo-internal

    Overall commission, market defaults, and no pre-picked units

    Three changes to how commission is captured. Nothing is pre-selected any more. Percent/Amount and One-time/Recurring both start unset on every commission split, so each one states its type because somebody said so. A pre-selected "Percent" is indistinguishable from a considered choice, and the difference between 15% of a sale and $15 is the whole payout. Submit stays disabled until a started row is finished. The approver now agrees an OVERALL commission — what the organization pays on the sale to the requesting agent and the group they share with, from which the per-colleague splits are carved. It appears in the approve and edit dialogs and as a column on the reserved list, and a market can carry a default that pre-fills it. Market defaults pre-fill the APPROVAL only. Seeding an edit from the market would let a save that was about dates stamp on a commission nobody agreed to, and would make "no commission" impossible to keep. Clearing a prefilled figure now sends an explicit clearCommission, tracked against what the form OPENED with rather than what is stored — otherwise deleting a prefilled default sends nothing and the server helpfully puts it straight back. Also: a half-filled market default now blocks the save instead of being dropped and reported as success; the commission column no longer invents "one-time" when no cadence was set; split amounts in the tooltip use the same money formatting as the column; and Add Market clears the quote fields it was inheriting from the last market edited.

  12. · SecurityService

    Agree an overall commission at approval, seeded from the market

    The approver now records what the organization pays on the sale to the requesting agent and their group. Omit it on approve and the lead's market default applies — that is what makes the market setting a default rather than a suggestion only the UI honours. applyCommission sets the three fields together and never merges them field-wise with what is stored. Merging looks harmless and is not: an edit carrying only a share type would flip a stored 15 from fifteen percent to fifteen dollars, re-denominating a payout nobody meant to touch. An invalid or half-configured market default is logged and ignored rather than thrown. A default is convenience, not law, and one saved through some other path with a nonsense figure must not turn into an unapprovable-market outage — the approver can still type a commission in, and approving without one is legitimate. clearCommission is honoured ahead of everything, including the market default: an approver who deletes a prefilled figure has made a decision, and quietly putting it back would reverse it. 49 tests, including the clear paths, the merge guard, and both bad-default cases.

  13. · InitializerService

    Commission columns for reserved opportunities and market defaults

    Six nullable columns: the overall commission agreed on a reserved opportunity, and the market default that pre-fills it at approval. NUMERIC(19,4) to match the split table — money is exact decimal or commission disputes follow. Applied against production on 2026-08-04; all six verified present.

  14. · kamo-shared-library

    The overall commission on a sale, and market defaults for it

    Fursa iliyohifadhiwa sasa inarekodi kile ambacho shirika hulipa nje ya uuzaji Kikundi cha maombi na kikundi ambacho wanashirikiana nao - sufuria ya per-colleague Vipande vinaondolewa. Soko la kuongoza hubeba default ambayo inatangulia wakati kukubaliwa. Kila kitu ni bure bila ya malipo. Kwa bahati mbaya hakuna mtu aliyekubaliwa Tume iliyokubaliwa na soko ambalo halijasanidi moja haina default; Kuandika sifuri katika aidha ingekuwa kuweka takwimu hakuna mtu alichagua kwenye rekodi ya malipo. Maombi matatu hayakuwa ya kutosha. JSON null na eneo la kutokuwepo ni kitu kimoja baada ya uharibifu, na kutokuwepo tayari kuna maana nyingine mbili Mambo hapa - "kutumia default soko" juu ya kuidhinisha, "kuacha peke yake" kwenye hariri. Hivyo basi, ni wazi kwamba mtu yeyote aliye na ulemavu wa ngozi hawezi Tume inapata kimya, na kwa kupitisha default wao tu kutatuliwa huja nyuma. LeadIdentity faida default soko na findIdentities sasa inaelezea yake wa wa wa wa wa wa wa wa wa wa wa Imeandikwa kama njia implicit l.market.x, Hibernate hutoa INNER kujiunga na kila kuongoza bila kushuka kwa soko kabisa - ambayo Ingeonekana kama "kiongozi kisichojulikana" kwenye safu ambazo hakuna mtu anayeangalia mara mbili.

  15. · kamo-internal

    Render the client's details on the preview, from the lead

    Removing the quote's own addressee fields left "Prepared for" blank, because customerName only ever came from a linked Account and lead-raised quotes have none. The preview now prints the block the server assembles from the lead. Company heads it when there is one — a quote to a business is addressed to the business, with the contact named beneath — then address, phone and email, each omitted when the lead does not carry it.

  16. · kamo-signer-monorepo

    Show the client's details on the customer's copy

    Company heads the block when there is one — a quote to a business is addressed to the business, with the contact named beneath it. Matches the staff preview exactly.

  17. · SecurityService

    Resolve the addressee lead for both the staff and customer views

    Org-scoped in both places. On the public page the scope comes from the share link's own organization, so a token can never surface a lead belonging to another tenant.

  18. · kamo-shared-library

    Put the client's details on the document, read off the lead

    Dropping the quote's own addressee fields left the document with nothing to say about the client: customerName only ever came from a linked Account, which lead-raised quotes do not have, so "Prepared for" printed blank. The addressee is now assembled from the lead — name, company, address, phone, email. It is a lookup rather than a join because the quote deliberately keeps only a soft leadId so the lead module stays independently deployable, so the controllers resolve it and pass the block in. A half-filled lead is the normal case, not the exception: leads arrive from vendors with whatever the source captured. The address collapses to one line and skips what is missing, so a lead holding only a city prints "London" rather than ", London, ". A lead that cannot be read yields no block rather than throwing — a quote whose lead was deleted should still render.

  19. · kamo-internal

    Move quotes out of commerce and onto the lead

    Quotes sat in the commerce pipeline, beside orders and carts rather than beside the client they are for. They now live on the lead: a Quotes tab directly after Client Lead Data, shown only where the market enables them. The standalone quote page is gone rather than kept behind a flag — the lead already supplies the header, the client's identity, its communications and its documents, so a second record layout on top of that duplicated the chrome and split the client's history in two. Selecting a quote slides the list out and the editor in, and the lead header never leaves the screen. Communications and Docs tabs are dropped for the same reason. Markets gain an Enable Quotes switch and the default terms their quotes start with. Without EDIT_QUOTE_TERMS the terms field is disabled AND omitted from the payload; the server strips it too, since a disabled input is presentation, not enforcement. Sub-tab ids lose "quotes"; its regression test now asserts the general property — every tab past the first keeps its own index — so it still guards the bounce bug without naming a tab that no longer exists.

  20. · SecurityService

    Expose per-market quote settings; enforce the terms grant

    uhakiki wa soko sasa unaelezea mtazamo wa kuongoza ikiwa kutoa tab ya nukuu na ni nini kinachosema quote mpya huanza na, na mwisho wa soko pande zote mbili. Mipangilio ya masharti ni kuvuliwa upande wa seva kwa waombaji bila EDIT QUOTE TERMS. Kukataliwa kwa uwanja katika UI ni uwasilishaji; PATCH iliyotengenezwa kwa mkono haipaswi kuwa na uwezo wa kuandika upya org ya kisheria pia.

  21. · kamo-shared-library

    Quotes belong to leads, configured per market

    Quotes were a commerce-pipeline artifact, which put them beside orders and carts rather than beside the client they are for. They now hang off the lead: the market decides whether its leads can carry quotes at all, and the default terms every quote in that market starts with. Per market rather than per org because a mortgage desk issues Loan Estimates under TRID and has no use for a generic quote, while a services market lives on them. Terms carry the org's legal commitment, so departing from the market wording on a single quote is its own grant — EDIT_QUOTE_TERMS. A null terms field already meant "leave unchanged" on update, which is what lets the controller strip it for callers without the right. Also drops the quote's own recipient name/address/phone: the lead already holds the client's details, and snapshotting them duplicated a record edited elsewhere that would drift from it. Schema applied by KamoInitializer ahead of this commit; columns verified.

  22. · SecurityService

    Reserve, approve and audit lead opportunity claims

    REST surface behind /leads/opportunities/reserved and the Reserve Opportunity control in the lead header, plus the service that owns the invariants. Rights, enforced explicitly because this service runs permitAll with no @EnableMethodSecurity — a missing check is a missing control, not a default-deny: VIEW_OPPORTUNITIES every read CREATE_OPPORTUNITIES raising a request, and asking for more time on your own EDIT_OPPORTUNITIES approving, editing, closing, ruling on an extension DELETE_OPPORTUNITIES "Deny Opportunity", which the UI puts behind a confirmation Deny is terminal but not destructive: the reservation, its splits and its whole history stay readable on the Denied tab. That is the point of denying rather than deleting. Details worth keeping: - The lead-header endpoint is readable with CREATE alone, since the button has to know which state to render — but a caller without VIEW who is not the holding agent gets a narrowed projection. The commission splits and justification that VIEW exists to protect are not disclosed by the widened gate. Rebuilt as an allow-list, so a field added later cannot leak by omission. - The list masks contact and company on unassigned leads for members without VIEW_UNASSIGNED_LEADS, matching LeadController. Who claimed what is not the secret; the prospect is. - Extending is gated on being the agent who holds the reservation, with no god bypass: holding a reservation is an identity fact, not a right, and an extension is filed under the requester's name. - A lapsed reservation that gets extended restarts rather than resumes, so reservedUntil == reservedFrom + approvedDays keeps holding — update() re-derives the end from the start, and a stale start would silently erase the grant. - The commission-split pool is VIEW_LEADS holders plus grant-all owners/admins, identical to what the picker endpoint offers. Computing it from the rights table alone made the dropdown offer an owner the backend then rejected. - Expiry is a scheduled sweep so the tabs can paginate on a real column, but every DTO reports daysRemaining against the database clock, so reads are correct between sweeps regardless. 36 tests cover the lifecycle, the date arithmetic and the split rules.

  23. · InitializerService

    Tables for lead opportunity reservations

    Mirrors LeadOpportunityReservation, LeadOpportunityCommissionSplit and **************** in kamo-shared-library. ddl-auto:update would create them from the entities anyway; the explicit DDL is the house convention and makes the shape reviewable here rather than only inferable from annotations. Applied against production on 2026-08-04 — all three tables and every index exist, owned by the app user.

  24. · kamo-shared-library

    Entities for time-boxed lead opportunity reservations

    An agent stakes an exclusive claim on closing a lead for a bounded window; an approver grants, edits or denies it, and colleagues can be offered a share of the commission. Three tables: the reservation, its commission splits, and an append-only audit trail. Design points that are load-bearing: - Lead, organization and member references are plain id columns with no foreign keys, matching LEAD_HISTORY and LEAD_TASK. The trail has to outlive lead deletion and member churn without either being able to block or cascade it. - dbNow() uses CAST(now() AS timestamp). CockroachDB's bare now() is TIMESTAMPTZ, which Hibernate 6 materializes as an Instant that cannot be cast to the declared LocalDateTime — and the ::timestamp shorthand is mangled by the native-query parser reading ':' as a parameter prefix. Neither shows up at compile time. - The paged list uses JpaSpecificationExecutor rather than JPQL with (:param IS NULL OR ...) guards, because CockroachDB rejects an untyped bind parameter in a bare IS NULL test. - LeadIdentity is a projection, not the Lead entity: Lead maps four @ManyToOne associations that default to EAGER, so listing 100 reservations by entity would fire hundreds of selects for data the grid never shows. - The history timeline orders by (dateCreated DESC, id DESC). CockroachDB's now() is the transaction timestamp, so rows written by one edit share it exactly; without the tiebreak an audit trail reshuffles itself between page loads. No stereotypes here — the owning service declares the beans, so no other service pays for the repository scan.

  25. · kamo-internal

    Address the quote to a person; summary stops costing header height

    Kuchukua sufuria kwa kupikia ukubwa wa kulia, tuma mboga kwao. Bonyeza chini ya jina. Kwa sasa kuna pengo ambalo limekuwepo kati ya jina na Hatua za haraka, kwa hivyo haina gharama yoyote. Tofauti hii ina mwisho, hivyo kidogo kuonyesha inline Na wengine kukaa click moja mbali - amri hubeba kumi na mbili na itakuwa umati nje vitendo. Inabakia kuonekana katika kila upana badala ya kutoweka kwenye skrini nyembamba. Nukuu pia inapata anwani yake: ni nani, kwa hiari yao anwani na simu. Hii ni sehemu ya kwanza kwa sababu ni jambo la kwanza kuamua, na wakati Akaunti imeunganishwa uwanja wa jina inaonyesha nini itakuwa vinginevyo kuanguka nyuma. Kwa kuwa Preview ya "Kuandaliwa kwa" block inatoa wote watatu na inaonyesha nini ni kuwa aina badala Ni kitu ambacho kiliokolewa mara ya mwisho.

  26. · kamo-shared-library

    Tuma barua pepe kwa mtu ambaye bado si akaunti

    The addressee was derived solely from a linked Account, so any quote raised for a prospect — which is most of them — printed a blank "Prepared for" line and the finished document did not say who it was for. The quote now carries the recipient's name, address and phone in its own right. What staff typed wins over the linked account: they have said what should be printed, and an account renamed later must not silently rewrite a quote that was already sent. Address is free-form because postal formats are not uniform enough to split into fields that work internationally, and it prints as written. A revision carries the addressee forward — same offer, same person. Schema applied by KamoInitializer ahead of this commit; columns verified present.

  27. · kamo-internal

    Open a member's profile from their row

    Adds a fourth action to the row's hover dock, linking to **************** It is a real NextLink rather than a click handler, so middle-click and "open in new tab" behave and the repo's no-hard-navigation guard stays satisfied; the panel closes on navigation so its body scroll lock does not outlive the route change. The action is gated the way the destination gates itself: the member settings page redirects you to your own profile unless you hold MANAGE_MEMBERS, MANAGE_MEMBER_SECURITY, CONFIGURE_SYSTEM, EDIT_MEMBER_ACCESS_LEVEL, ALLOW_PROMOTING_MEMBERS or **************** (or god / platform admin). Without one of those the icon is hidden rather than offered as a dead end. Your own row always shows it.

  28. · kamo-internal

    Fail the build when a component calls a key messages/ does not define

    next-intl renders the key path verbatim for a missing key, and messages/ is gitignored here — populated at image-build time from kamo-translation-dictionary — so a rename that lands in only one of the two repos ships as visible gibberish with nothing to catch it. That is how the department contact fields came to render **************** on the settings screen, and how the entire HMDA demographics form (39 labels) shipped against a namespace that only contained `lar`. scripts/check-i18n-keys.mjs resolves each translator binding's namespace, then checks every literal key, the static prefix of every template-literal key, and the values passed against the {placeholders} in the message — in both directions, since an unsupplied ICU argument throws MissingValueError and an unused one silently drops data. It understands ICU brace escaping and plural sub-messages, and treats <tag> as a placeholder only for t.rich/t.markup. 1737 files in ~1.4s. Wired into `npm test` and into the Dockerfile ahead of the build, with --require-messages there so a skipped dictionary sync fails loudly instead of turning the guard into a no-op. Typing t() against `typeof en` was tried first and rejected: a JSON import widens values to `string`, so next-intl cannot infer placeholders and rejects correct calls like t('...range', {start, end, total}); it also tripled typecheck time (81s -> 249s) and would have needed ~150 edits to non-buggy code. Call-site fixes for what it found: - dropped a `default:` option passed to t() in the chat components and LanguageButton; next-intl has no such fallback option, so it was dead weight. - commerce lowStock passed {count} for {available}, MultiFileUploader {types} for {list}, and audioVideo master volume {pct} for {percent} — all three threw at format time. - DnsSetupStudio passed a {domain} its subtitle does not use.

  29. · kamo-internal

    Pin/unpin nav options from the navs and the home launchpad

    Members could move an option between the side bar and the top bar, but never take one out of the navs entirely. Adds a third placement — NONE, "unpinned" — and two ways to reach it, both driving the same shared menu: - right-click a movable option in NavPri/NavTop: the placement menu opens at - the corner control on each home-launchpad pane: a quiet chip that reads out where the option currently sits and expands on hover, opening the same menu Unpinned is never a dead end. The home launchpad now renders from every gated option rather than the union of the two navs, so an unpinned option keeps the pane that pins it back; the command palette does the same, and each change raises an undo toast naming where the option went. The Navigation Layout settings page grows a matching Unpinned column. The resolver keeps its invariant — every gated option in exactly one bucket — and an explicit NONE now beats a stale zone order, so an unpin survives a blob written before it. Legacy stored layouts have no NONE entries and resolve exactly as before; Reset to Standard can never leave anything unpinned. Covered by new tests for resolveNavLayout, which had none.

  30. · kamo-internal

    Hide empty departments, move the status filter into the Directory

    Nchi ambazo hazina wanachama hazitumiki tena. Mwisho wa Uwasilishaji Inarudi kila idara katika org ikiwa ni pamoja na tupu, hivyo orodha ilikuwa Kuonyesha sehemu ambazo haziwezi kuwa na mtu yeyote. Sehemu ya "Hapana" "Wamiliki wa nyumba wanashirikiana nao, na kwa sasa hawana uwezo. Filter ya All / Online / Offline inatoka kwenye kichwa cha jopo chini ukanda tab, hivyo ni anakaa katika eneo Directory mwenyewe na orodha ni filters badala ya kuonekana kuchuja jopo nzima (haijawahi kutumika kwa mawasiliano au mawasiliano ya simu). - Nchi tupu inaonyesha tu kurekebisha utafutaji / filter wakati mmoja Kwa kweli kutumika; org bila wanachama hakuna kitu cha kurekebisha.

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