Nenda kwenye maudhui
KAMOUniverse

Mabadiliko

Nini kipya.

Kila kuboresha jukwaa, kama ni meli.

  1. · kamo-internal

    Show every storage category separately

    Call recordings, voicemails, chat attachments, HR resources and marketing materials each get their own line instead of sharing one "Documents and images" row with everything else that happens to live in the same table. Two consequences for the visuals. Fourteen categories exceed what any categorical palette can encode — past about eight, added hues stop being separable for colour-blind readers and become decoration — so identity is carried by the label and the share bar uses one hue purely as a magnitude cue. And the trend is now a single total series rather than a fourteen-way stack: the question a trend answers is "is my storage growing", and which category it grew in is the table's job, which the table does exactly.

  2. · kamo-shared-library

    Count each kind of stored file as its own category

    Call recordings, voicemails, chat attachments, HR resources, marketing materials, meeting recordings and every kind of document are all rows in imgs, distinguished only by ImageAssocType and all sharing one bucket, because association is metadata rather than a storage boundary. Reporting them as a single line was accurate and useless: for one production organization it hid that 5.08 GB of a 5.09 GB footprint was chat attachments. Each association now belongs to a domain, so an organization can see what is actually consuming its storage rather than one undifferentiated total. StorageDomainCoverageTest makes an omission a build failure: every ImageAssocType must be claimed by exactly one domain — unclaimed means uploads are stored and never billed, claimed twice means billed twice. Verified by probe: removing HR_RESOURCE from its domain fails the build with the reason, rather than passing quietly. MEETING_RECORDINGS sums both its sources, the meeting_recordings table and the MEET_RECORDING association. MeetRecordingService writes only the former, so they do not overlap.

  3. · EmailService

    Measure each org's own KamoMail mailboxes

    Walks every mailbox Kamo hosts and records, per arrival day, the RFC822 bytes it holds — headers, body and encoded attachments, across every folder including Trash and Junk, since a message in Trash is still on disk. One round trip per folder: the Jakarta Mail IMAP provider expands FetchProfile.Item.ENVELOPE to ENVELOPE INTERNALDATE RFC822.SIZE, so size and arrival date arrive together. What counts is decided by whether Dovecot has credentials for the address, not by reading the provider setting. Presence in virtual_users IS the definition of "Kamo stores this mailbox", whereas a provider column can be stale, inherited from a parent, or mid-migration. A mailbox hosted by Microsoft or Google yields no credentials and is skipped, so its organization is charged nothing. Who it counts for comes from each mailbox's own org_id. KamoMail is not a single tenant — production has 18 mailboxes under one organization and 41 under another, intermixed across four domains — so summing a domain or the maildir would bill one organization for another's mail. Addresses are matched lowercased. Dovecot's user table and maildir paths are lowercase while the mailbox table keeps whatever casing the mailbox was created with, and a case mismatch here has already once split delivery and retrieval across two different maildirs. Also protects /api/internal as a whole rather than endpoint by endpoint. The filter named /api/internal/calendar and /api/internal/lead-comms individually, so the next controller mounted under that prefix — this one — would have been reachable with no secret, silently and in the direction of exposure. 6 new tests covering attribution, case handling and the not-hosted case; 152 total, all passing.

  4. · InitializerService

    Schema for per-mailbox storage measurement

    **************** holds, per mailbox and per arrival day, the RFC822 bytes that mailbox is holding. Storage accounting had no honest source for mailbox bytes: the search index covers only messages that were indexed, and covers them regardless of who stores them. ORG_ID is denormalized from email_provider.mailboxes so per-organization aggregation is one indexed sum, and so attribution stays per mailbox — several organizations can be on KamoMail at once and one domain's maildir can hold mailboxes belonging to different organizations. Run against production 2026-08-08; table and indexes verified.

  5. · kamo-shared-library

    Measure the mail Kamo actually hosts, per mailbox

    EMAIL was sourced from email_metadata.search_index, which was wrong in both directions. It missed KamoMail entirely — an org on KAMO_MAIL has its messages and attachments in the cluster's own Dovecot maildir, 62 MB of it in production, counted as zero. And it would have charged an org on Microsoft 365 or Google Workspace for mail sitting on that provider's disks, where Kamo holds nothing but a search index of its own. **************** replaces it: per mailbox, per arrival day, total RFC822 bytes — headers, body and encoded attachments, which is what occupies the maildir. Bucketing by day rather than per message keeps the table proportional to days-with-mail while still letting an arbitrary billing window be summed exactly instead of rounded to whole months. Attribution is per mailbox via its own org_id, never per domain and never per maildir. KamoMail is not a single tenant: production already has 18 mailboxes under one org and 41 under another, intermixed across four domains in one table. Summing a domain, or the maildir as a whole, would bill one org for another's mail. Schema verified in production before this landed: email_provider .mailbox_storage_days exists with its uniqueness constraint after a KI run.

  6. · kamo-internal

    Say why mailbox storage can read zero

    Mailbox bytes count only the mail Kamo actually hosts. An organization on Microsoft 365 or Google Workspace has its messages on that provider's disks, so its Mailboxes row is legitimately 0 — which, unexplained, reads as a broken number rather than a correct one.

  7. · BillingService

    Let a platform admin run the snapshot sweep on demand

    The sweep is what makes period removals and lifetime totals for the hard-deleting domains answerable at all, so days lost to an outage are lost permanently unless the gap gets filled. Waiting for the next 04:20 run is not a recovery plan. The sweep already catches up 14 days and is idempotent per (org, day, domain), so repeated calls settle rather than double-count. This just exposes it, behind @PlatformAdminOnly, and returns the row count so the caller can see it worked.

  8. · kamo-internal

    Show storage usage on the account stats tab

    The tab said "No usage data available for this period" for every organization. billingApi.getCurrentUsage() is a stub returning { meters: [] }, and UsageMeters renders its empty state at zero meters — so the message was reporting the stub, not the data. StorageUsagePanel reads the real BillingService endpoint and shows the two figures that are easy to conflate side by side: what the organization is holding right now, which falls when files are deleted, and what it has added over its lifetime, which does not. Alongside them, what moved this period, and a per-domain breakdown across documents, mailboxes, knowledge-base media, meeting recordings and disclosures. Where a figure is a floor rather than a measurement, it says so — a removal total with no opening snapshot behind it, or a lifetime total for a storage area that hard-deletes its records. A chip explains the record basis: an org is counted for every file it holds even though Kamo stores one copy per hash. The trend renders only once two days of history exist; one point is not a trend, and drawing it would imply a history that has not been collected yet. Series colours are the validated categorical order — worst adjacent CVD ΔE 9.1, worst adjacent normal-vision ΔE 19.6 on this surface. Three sit below 3:1 contrast, so the breakdown table is not decorative: it is the non-colour reading of the same data. Byte counts stay strings end to end (BillingService serializes Long via ToStringSerializer) and are only widened where the magnitude is provably safe.

  9. · BillingService

    Report what an organization is storing

    The account stats tab has always said "No usage data available for this period", for every organization, because getCurrentUsage() in kamo-internal is a stub returning zero meters and nothing behind it ever aggregated storage. Serves three distinct figures per storage area, plus a total: - what the organization holds right now, which falls when files are deleted - what it has added over its lifetime, which does not - what moved during the current billing period Removal is derived — no storage table timestamps a deactivation, since EmbRecordState.DATE_UPDATED is insertable=false and never refreshes — as opening + added - current against the period's opening snapshot. With no opening snapshot it falls back to trash timestamps and reports itself as a floor rather than presenting a guess as a measurement. Lifetime is exact where removed rows survive and accumulates from snapshots where they do not; both say which they are, so the UI can too. BillingPeriodResolver prefers a live subscription's period and falls back to the calendar month in the org's timezone. That fallback is the whole feature in production: every subscription there carries NULL current_period_start and current_period_end, so without it the period figures would be empty for every organization that exists — the same class of bug as the stub it replaces. StorageSnapshotSweep writes the daily position each org's history depends on, catching up to 14 missed days and seeding a first snapshot from the queryable lifetime so tracking does not restart at zero. The platform endpoint reports billed against physically stored bytes. Customers are billed per record; img_dats holds one copy per hash however many orgs have that file, and thumbnails and conversions cost bytes nobody is billed for. Both gaps are Kamo's economics, so they sit behind @PlatformAdminOnly. 21 new tests, 113 total, all passing.

  10. · InitializerService

    Schema runner for org_storage_snapshots

    Mirrors OrgStorageSnapshot as explicit DDL, per the house style — ddl-auto creates the table first, so the CREATE is a fallback and the SET DEFAULTs are what actually make it usable from plain SQL (Hibernate emits no column defaults for values the entity fills in Java). Also indexes imgs on (ORG_ID, DATE_CREATED) and (ORG_ID, DATE_TRASHED). Storage aggregation reads both on every summary request and the existing indexes are all assoc- and folder-shaped. Run against production 2026-08-08; org_storage_snapshots verified with all 16 columns and its uniqueness constraint before the shared-lib entity was pushed.

  11. · kamo-shared-library

    Per-organization storage usage model

    Nothing has ever summed storage per organization. Six entities carry a byte count per file, and no code aggregated any of them, which is why the account stats tab renders "No usage data available for this period" for every org. Adds the shared model that aggregation needs: - StorageDomain — the five org-attributable storage areas, each declaring whether its removed records survive. Documents and KB media soft-delete, so a lifetime total is a straight query; mailboxes, recordings and disclosures hard-delete, so theirs can only accumulate forward. - OrgStorageSnapshot — one row per (org, day, domain). It exists for three figures that are unrecoverable after the fact: lifetime totals for the hard-deleting domains, the opening position that period removals are differenced against (no table timestamps a deactivation — EmbRecordState's DATE_UPDATED is insertable=false and never refreshes), and a period-close number fixed when the period ended. - OrgStorageAggregator — every per-domain aggregate in one class. Totals are summed over records, never over stored objects: img_dats keeps one physical copy per hash however many orgs hold that file, and that saving is Kamo's. The aggregator deliberately carries no stereotype. Every service runs @ComponentScan("com.kamo"), so annotating it would construct it in all ~40 of them; BillingService declares the bean. Also adds the search-index queries the email size backfill needs — size_bytes was written as a hardcoded 0 since the index was created, so mailbox storage reads as zero bytes platform-wide until those rows are repaired. Schema verified in production before this landed: org_storage_snapshots exists with all 16 columns after a KamoInitializer run.

  12. · InitializerService

    Declare the **************** app split

    The backfill only ever filled a plan row that was missing — deliberately, so it could not silently move a tier — which left apps that had never been priced wherever it first put them. Point of Sale and Marketing Tools ended up on Free that way, which is not what Free is sold as. Anchored to what each plan already promises on the pricing page, so the matrix and the marketing copy cannot drift: Free is evaluation (CRM, chat, docs, notes, KB, calculator); Starter adds the apps a business trades with (support, commerce, marketing); Business adds the premium set plus telephony, inventory and clubhouse; Enterprise adds Legal. Email hosting and the two origination systems belong to no tier — email is priced per seat on any plan, the origination systems are negotiated per lender. Unlike the backfill this reconciles rather than fills gaps, which is the point of being the source of truth: a tier change belongs here, in review, not in a UI edit the next run would revert. Applied to prod: 11 rows changed. POS and Marketing off Free, and the four unreleased apps given the tier they will land on when they ship, so publishing one is a stage flip rather than a data change. A second run reports zero changes.

  13. · kamo-internal

    Split Apps & Features into Available / Not available / Deprecated

    One flat list mixed apps the platform offers with ones it does not and ones it has retired, so the operator had to read every chip to tell them apart. The buckets are exclusive and deprecation is tested first: a retired app is retired whatever its availability flag still says, so it appears only under Deprecated. That also makes the per-row "Deprecated" chip and the dimming redundant — every row in that tab is retired — so both are gone. Partitioning reads server state rather than the draft, so ticking the deprecated switch leaves the row in place until the save lands instead of yanking it out from under the edit.

  14. · SecurityService

    A tenant-readable app catalog for Branch Types

    Branch Types -> Allowed Apps started loading its app list from /platform-config/app-catalog, which requires a top-level org owner. The host page is gated on CONFIGURE_SYSTEM instead, so for every tenant the GET 403'd: the editor rendered zero apps and the save handler then refused to save the security model at all — blocking its name, age ranges, roles and commerce types, none of which involve the catalog. It failed for a KamoCRM admin without isOrganizationOwner too. **************** serves the same resolved catalog under CONFIGURE_SYSTEM, as a deliberately narrower projection: type, display name, description, stage, available, deprecated. No operator note, no enum defaults, no entitlement diagnostics — those describe how the platform is run, and the writes stay platform-owner-only.

  15. · kamo-internal

    Add an Apps & Features tab and fix the Allowed Apps list

    The new tab, to the right of Organizations, controls what every organization is offered: availability, development stage, display name, description, an operator note, and deprecation. Availability is no longer derived from stage, so an app can be published while still in testing or held back after it is finished; stage now drives only the badge and the bar. Overridden fields are marked and revert individually, and deprecation — which hides an app from every tenant using it — needs the app's name typed to confirm. Allowed Apps in the security-model editor was a hand-maintained TypeScript copy of ServiceType.java, with a comment asking editors to keep it aligned. It had drifted, invisibly: - MLOS was missing entirely, so no security model could grant mortgage origination at all. - LOS was labelled "Mortgage Loan Origination System" — it means personal loans since the 2026-08-08 rename, so the editor wrote a personal-loans grant for an operator who thought they were granting mortgage. - POS and ESIG showed PLANNING long after they shipped. Adding availability indicators to that list would only have made it confidently wrong, so it now reads the same catalog the new tab writes and cannot disagree with it. Saving is blocked until the catalog loads: every appConfigs builder iterates it, so an empty list would have stripped the model's grants.

  16. · SecurityService

    Serve the app catalog and read features through it

    FeatureController derived "is this app offered" from the enum's stage in four separate places, so the answer could only be changed by redeploying the platform. Every one of them now reads AppCatalogService, along with the display name, description and deprecation. The one behavioural subtlety is the root-org gate. It used to read "stage is not COMPLETED, so child orgs may not enable this" — the dogfooding path. It now reads "the platform does not currently offer this". While no override exists those are the same sentence, because unset availability resolves to stage == COMPLETED. Once an operator publishes an app that is still in TESTING, only the new form does the right thing: without it the app would stay silently blocked for every child org. PlatformAppCatalogController is gated to top-level-org owners, the same audience as the neighbouring platform tabs, and reports whether an available app is actually in the plan matrix — an app no plan mentions resolves through the fail-open setting rather than through anything the operator chose.

  17. · kamo-shared-library

    Make app availability and stage editable at runtime

    Every attribute the apps settings page renders — display name, description, stage, deprecation — was a compile-time constant in the ServiceType enum, and "Available" was not stored at all: it was derived as stage == COMPLETED in the UI and in three places server-side. Changing any of it meant editing the shared library and redeploying the platform. PLATFORM_APP_CATALOG holds one row per app with every override column nullable, and AppCatalogService resolves override ?? enum. A missing row means the enum verbatim, so installing this changes no behavior. Availability becomes its own value, which is what makes a soft launch (offered while still TESTING) or a withheld COMPLETED app expressible. Its fallback reads the RESOLVED stage rather than the enum's, so an operator who only moves an app to COMPLETED gets availability without also flipping the switch.

  18. · InitializerService

    One properly categorised catalog row per app, on every entitlement root

    Nine apps — POS, Marketing Tools, Calculator, Club, Inventory, Legal, Games and the two origination systems — had no catalog representation at all: no pricing row, no category, no tier. Twelve others were already described by a marketing row, so binding those and adding a parallel APP_* row would have given one ServiceType two rows and made planFeatureIncluded depend on iteration order. The runner now converges the catalog instead: - binds a published marketing row to the app it describes (VIDEO_CONF is Meet, MESSAGING is Chat), keeping its code, name, category and tier values so the published matrix does not move; - creates a row for an app the catalog does not describe, in a real category — including a new Commerce & Origination category; - promotes any earlier APP_* placeholder to its real identity and deletes the flat Applications bucket once empty; - removes duplicate rows for the same app, preferring the marketing row. There is no APP_ fallback left: every app has a readable code, and apps the published catalog already names reuse that exact code so an org with its own matrix and one without describe the same product identically. That mattered — sign.pink has no marketing matrix and was left with eleven rows named after enum constants. MLOS and Personal Loans are enterprise-negotiated, not per-seat: null price, FLAT pricing model, compatible only with CUSTOM-type plans (Enterprise), bundled into no tier. They are created on the platform org only — a white-label product org does not resell them, and the two created on sign.pink by an earlier pass are deactivated. Verified against prod after running: 0 APP_* codes left, 0 duplicate service_type rows, 21 apps on each root, 0 uncovered plan-by-app combinations, and a re-run is a no-op.

  19. · kamo-internal

    Server-paginated organizations console

    The tab loaded every organization in one request and rendered a card each, with the backend fanning out per org, then per account, then per subscription. Search, sort, filter and paging now run server-side against the whole estate, so the page cost is flat no matter how many organizations exist. The row is also the right object now. A subscription is HELD by an account and TARGETS an org, so the old card -- keyed on the holder -- had to print the target's name inside a nested table, and customer orgs appeared as strings inside other orgs' rows rather than as rows. Each row now carries entitlement (inbound: what governs this org) and billing held (outbound: what it bills for others) as separate facets. Design: a status rail down each row's left edge and a proportional estate bar, so a page reads as a colour histogram of commercial health before a word is read; tabular numerals with zeros muted, since most cells on a billing estate are zero. Table state lives in the URL so a filtered view is linkable -- written through a copy that preserves `tab`, and without publishing activeTab, which belongs to tabSync.

  20. · SecurityService

    Fail closed on apps a plan never mentions

    Turns kamo.entitlement.fail-open off now that AppEntitlementCatalogMigration has run against prod and every active plan on both entitlement roots (KamoCRM, Sign Pink) carries an explicit row for all 21 non-core apps — verified zero uncovered plan-by-app combinations, so there is nothing left for a deny-by-default to catch except a genuinely unpriced app. This does not change what any org can use today: shipped apps are explicitly included, and MLOS and the personal-loan app were already denied by explicit isIncluded=false rows, which bind regardless of this flag. What it closes is an app slipping through unpriced by never appearing in a plan's matrix at all. Flip back to true if a new entitlement root is added before the migration has run for it.

  21. · BillingService

    Serve the org directory as a filtered, sorted, paginated page

    GET /orgs takes page/size/q/sort/dir plus seven filters and returns the house page envelope with estate-wide aggregates. GET /orgs/{orgId} serves full detail for the console's drill-in drawer, where the per-account fan-out is fine because it is bounded to one org. listAllOrgs() is replaced rather than kept as a truncating delegate: it had exactly one caller (this controller), and the frontend's listOrgs() had exactly one (the tab being rewritten), so a compatibility shim would only have added a silent-truncation risk with nothing depending on it.

  22. · BillingService

    Batched page hydration at a constant query count

    Parent titles, primary domain, the plan/market behind the entitlement and the subscription-catalog flag are each one batched IN (:ids) over the whole page, so a 100-row page costs the same six statements as a 1-row page. The test asserts that equality directly -- the implementation this replaces would score 6 against 600. The governing subscription is picked by EntitlementModeResolver, the same code that decided the row's mode, so the plan shown always belongs to the state shown. Queries go through EntityManager rather than new repository methods so kamo-shared-library, which every service depends on, does not have to change.

  23. · BillingService

    Directory row/page DTOs and the native query repository

    PlatformOrgRow carries the two facets separately: entitlement (inbound subs targeting the org -- what governs it) and billing held (accounts the org owns -- non-zero only for the platform org and resellers). Aggregates describe the whole filtered set, not the page, so the KPI strip does not change meaning as the operator pages. Every driver value is coerced through Number/Boolean rather than cast: a coalesce(...,0) that matches no rows returns Integer where the same column with rows returns Long, which would pass in dev and throw in prod. Money crosses the wire as a decimal string.

  24. · BillingService

    Org directory SQL with per-facet CTEs

    Held accounts, inbound subscriptions, members, licensees and children each aggregate in their own CTE before being joined to orgs. Joining them in one pass would multiply rows and inflate every SUM. Pure and unit-tested (26 tests): no '::' casts, every value bound, sort keys whitelisted through OrgSort. Two things the tests pin because they are easy to "tidy" into bugs -- ORDER BY and the derived predicates (mode_rank, has_stripe) must name the base CTE's projected columns, since the `o` alias is out of scope in the outer query; and a PENDING trial reports no deadline rather than the latest expired one. Verified against the live cluster: both statements execute, the count agrees with the rows, and filters/aggregates apply. Index usage is NOT demonstrated -- at 12 orgs / 9 subs CockroachDB full-scans regardless, correctly.

  25. · BillingService

    Validated, clamped org directory request

    Size clamps to [1,100] and an unrecognised sort falls back to title, so a hand-edited URL can neither request the whole estate nor reach the SQL as free text. The OrgSort enum is the sort whitelist.

  26. · BillingService

    Derive entitlement mode through one shared, deterministic resolver

    TrialAdminService picked its trial subscription with findFirst() over an unordered list, so an org holding both a pending and an active trial could report either. The upcoming organization directory has to sort and filter on this state, and a query cannot reproduce a nondeterministic rule. The precedence now lives in **************** among live trials, active wins over pending wins over expired, and the active expiry reported is the latest one. status() keeps its exact response shape. First tests in this repo (11).

  27. · InitializerService

    Backfill app entitlement rows into the platform plan catalog

    Entitlement now resolves on a typed service_type column, but no catalog row carried one, so every plan still resolved to 'everything included' via the fail-open default. This seeds the matrix so kamo.entitlement.fail-open can eventually be turned off without dark-firing apps. - AppEntitlementCatalogMigration (@Order(51), after PlatformOwnerPlanSeeder): idempotent backfill. Creates an 'Applications' feature category with one BOOLEAN row per non-core app bound to its ServiceType, re-binds any pre-existing row that has the code but no serviceType, and inserts the missing plan-to-app rows per tier. Insert-if-missing only — it never deletes and never overwrites an operator's isIncluded, so a tier edited in the settings UI survives reboots. Also re-binds the existing VIDEO_CONF / AI_ASSISTANT / HR_SYSTEM / ESIGNATURES / EMAIL_HOSTING add-ons, which were granting nothing. - KamoCRMPricingLoader: same matrix for a fresh cluster. The app rows are appended inside upsertFeatures rather than written in a second pass, because upsertPlanFeatures deletes the plan's rows before inserting — a second call would silently wipe the marketing matrix. Note this loader is disabled (early return, and its DataLoader call is commented out), which is why the migration above is the production path.

  28. · SecurityService

    Gate mortgage and personal-loan verticals on app entitlement

    A MORTGAGE market appeared on /commerce whenever POS was on, regardless of the MLOS app, and /features/enable had no billing check at all — the padlock in the settings UI was presentation only, so a direct POST switched on an unentitled app. - FeatureController: 403 on enable when the plan does not include the app; serialize an 'entitled' verdict on both features[] and available[] so the UI padlock and the 403 are computed by the same service and cannot disagree. - CommerceMarketController: GET /allowed-types for the market-type picker (model allow-list intersected with entitlement); the market list drops unentitled verticals — hidden, never archived, so restoring the plan brings the market back with its data intact. - **************** fail-closed guard over every /{marketId}/... sub-route. AppAvailabilityInterceptor cannot express this — its prefix map is one prefix to one ServiceType and /commerce-markets serves all seven verticals under POS. - POSController: public callers of the subscription catalog no longer see rows bound to an app that has not reached COMPLETED, so moving a ServiceType to COMPLETED is what publishes it to the marketing site. Internal callers still see everything in order to price it beforehand. - MortgageVerticalResolver: readiness measured POS only, so an org that never bought MLOS still read as ready; it now reports MLOS in 'missing' too.

  29. · kamo-internal

    Rename LOS to MLOS, add Personal Loan Origination System, gate commerce verticals

    Apps tab now shows 'Commerce: Mortgage Loan Origination System' (the title is served from the backend enum, not the TS mirror) and a new 'Commerce: Personal Loan Origination System' under Not Available. MORTGAGE and PERSONAL_LOANS markets are gated on their app. - AppType/ServiceType mirrors: MLOS(14) + new LOS(25); also fixes the pre-existing drift where the mirrors disagreed with the Java enum. - Organization: isMlos added; isLos now means personal loans. isLos had zero consumers. - FeaturesManager: gating reads the server-supplied 'entitled' flag. It was rebuilt client-side from billingApi, whose getCurrentSubscription throws and getAvailablePlans returns [] (legacy stubs), so every app silently resolved to 'ungated' and no padlock ever rendered. - Market pickers: both the create and edit pages held their own hardcoded COMMERCE_TYPES array; they now render GET **************** so a vertical the org has not bought is not selectable. The edit page had also been skipping the one-MORTGAGE-per-org rule. It keeps the market's current type selectable so a lapsed org can still open and rename it. - Mortgage settings surfaces gate on isMlos rather than isPos.

  30. · kamo-shared-library

    Resolve app entitlement by ServiceType; rename LOS to MLOS

    Entitlement resolved ServiceType.name() against **************** The catalog's codes are marketing codes (USERS, VIDEO_CONF, HR_SYSTEM, ...) and never matched, so planFeatureIncluded's missing-row default granted every app on every plan — the billing-to-app link was inert. Catalog rows now carry a typed nullable service_type and resolution goes through it, so an app can be renamed without moving entitlement. - **************** typed service_type column (persisted as the id). - **************** resolve by ServiceType; honour add-ons via the previously unread **************** jsonb; missing-row behaviour behind kamo.entitlement.fail-open (default true until the catalog matrix is seeded). - SubscriptionService: carry serviceType through create/update/DTOs, and derive feature_code from the name when omitted — the settings dialog never sent one, so every create from the UI hit the NOT NULL constraint. - CommerceType: bind each vertical to its required app (MORTGAGE->MLOS, PERSONAL_LOANS->LOS). - AppliedModelEnforcementService: getAllowedCommerceTypes intersects the security-model allow-list with entitlement; assertCommerceTypeAllowed already covers create and edit. - ServiceType: LOS -> MLOS keeping id 14 (ORG_FEATURES persists the id, not the name, so no data migration); new LOS(25) for personal loans — id 25 because reusing 14 would reassign every existing mortgage row. - RoleRightType: MLOS_* rebound, ids 139-144 unchanged; personal-loan LOS_* added at 190-195. - PhiModule.PLOS added: the rename left the personal-loan app mapped to the mortgage PHI module and mortgage unmapped, which compiled clean.

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