@@ -0,0 +1,279 @@
|
||||
<!-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 -->
|
||||
# Business service modules
|
||||
|
||||
ConusAI Cloud runs business services next to the platform: Inventory, CRM, Catalog, Activities, Invoicing,
|
||||
Ledger and Archive. Each keeps its data in your organization on the platform and is called through the
|
||||
platform API with a generated client, `@conusai-sdk/<service>`. This template ships optional modules that use
|
||||
them -- `modules/inventory` (items, stock, reservations), `modules/crm` (parties and how to reach them),
|
||||
`modules/catalog` (products and their imports) and `modules/archive` (documents, versions and retention) --
|
||||
plus `modules/activities` (project tasks, discussion and timeline). All ship **off**.
|
||||
|
||||
Source status: request-bound adapters and simulated contract checks exist. The gateway-backed
|
||||
request composition, final release and live acceptance are still being integrated. Including a
|
||||
module does not establish readiness or authorize processing.
|
||||
|
||||
## Turn a module on
|
||||
|
||||
```bash
|
||||
pnpm kit module:on inventory # or crm, catalog, archive, activities; `pnpm kit module:off <id>` turns it off again
|
||||
pnpm openapi # the module's operations join openapi.json
|
||||
pnpm format # reflow edited configuration before the lint gate
|
||||
pnpm gate
|
||||
```
|
||||
|
||||
`module:on` does two things and prints what it changed:
|
||||
|
||||
1. adds the id to `selected` and `everAdopted` in `modules/inclusion.json`: the module joins the
|
||||
registry, its procedures the API, its page the project's navigation;
|
||||
2. asks the deployment token for the service's scopes in `conusai.json` (`token.permissions`, and every
|
||||
supported profile's copy (the Convex preview is refused)):
|
||||
|
||||
| Module | Scopes |
|
||||
|---|---|
|
||||
| `inventory` | `inventory.items:read`, `inventory.items:write`, `inventory.stock:read`, `inventory.reservations:read`, `inventory.reservations:write` |
|
||||
| `crm` | `crm.parties:read`, `crm.parties:write`, `crm.contacts:write` |
|
||||
| `catalog` | `catalog.products:read`, `catalog.imports:read`, `catalog.imports:write` |
|
||||
| `activities` | `activities.activities:read`, `activities.activities:write`, `activities.comments:write` |
|
||||
| `archive` | `archive.documents:read`, `archive.documents:write`; hold, retention and disposal administration need separate reviewed grants |
|
||||
|
||||
The deployment requests the scopes; an organization owner/admin must approve the permission delta separately. The service must also be **enabled for the
|
||||
deployment's organization** on the platform (Settings, Modules, or `conusai modules enable <service> --org
|
||||
<slug>`); until it is, the module's page says so instead of failing.
|
||||
|
||||
`module:off` removes selection while retaining historical custody and existing scope requests for
|
||||
cleanup. It does not revoke an approved credential. The request ownership record remains
|
||||
`.conusai/module-scopes.json`; customer-owned and historical untracked requests remain untouched.
|
||||
Physical source stays and is type-checked either way. Removing never-adopted source requires review;
|
||||
an adopted module must retain its original schema and rights/retention implementation.
|
||||
|
||||
## What the modules do
|
||||
|
||||
| Module | Page | Procedures (kind, app permission) |
|
||||
|---|---|---|
|
||||
| `inventory` | `/<org>/<workspace>/<project>/inventory`: the organization's items, a form that adds one, this project's open reservations with a release button; `/inventory/<itemId>`: the item's stock by location and a form that reserves some for this project | `inventory.item.list` (read, `inventory.item.read`), `inventory.item.create` (effect, `inventory.item.write`), `inventory.item.availability` (read, `inventory.item.read`), `inventory.reservation.list` (read, `inventory.item.read`), `inventory.stock.reserve` and `inventory.stock.release` (effect, `inventory.stock.reserve`) |
|
||||
| `crm` | `/<org>/<workspace>/<project>/crm`: the organization's parties and a form that adds one; `/crm/<partyId>`: the party's contact points and a form that adds one | `crm.party.list`, `crm.party.get` (read, `crm.party.read`), `crm.party.create`, `crm.contact.add` (effect, `crm.party.write`) |
|
||||
|
||||
| `catalog` | `/<org>/<workspace>/<project>/catalog`: the organization's products and a search; `/catalog/<productId>`: one product; `/catalog/imports`: the imports so far and a form that applies a CSV file | `catalog.product.list`, `catalog.product.search`, `catalog.product.get`, `catalog.import.list` (read, `catalog.product.read`), `catalog.import.apply` (effect, `catalog.product.write`) |
|
||||
| `archive` | `/<org>/<workspace>/<project>/archive`: the organization's documents and a form that registers one; `/archive/<documentId>`: the document's versions, a form that adds one, its legal hold and disposal; `/archive/retention`: the retention classes, a form that adds one, and what is due for disposal | `archive.document.list`, `archive.document.get`, `archive.retention.list`, `archive.disposal.list` (read, `archive.document.read`), `archive.document.register`, `archive.version.add` (effect, `archive.document.write`), `archive.document.hold`, `archive.document.dispose`, `archive.retention.create` (effect, `archive.retention.manage`) |
|
||||
|
||||
`inventory.item.read`, `crm.party.read`, `catalog.product.read` and `archive.document.read` are held by every
|
||||
member (viewer); `archive.retention.manage` by administrators, because a hold, a disposal and a retention policy
|
||||
change what the organization keeps; the others by editors. A change is an `effect` procedure: it calls a service and
|
||||
writes nothing of this app's own, so none of these modules has a table, a migration, a job or an event.
|
||||
|
||||
**Reservations belong to a project.** The service holds a reservation against a reference, and the module
|
||||
uses the project's: `conusai://app-partitions/{binding UUID}/projects/{resolved app project UUID}`. A project lists only its
|
||||
own reservations, and releasing another project's answers not-found. A form sends a request id the module
|
||||
turns into the service's idempotency key, so a retried request reserves (or creates) once.
|
||||
|
||||
**An import is one file.** The form reads a CSV file with the columns `code`, `name` and `base_unit` (`description`
|
||||
and `revision` are optional), hashes it, and sends it in chunks of 200 rows, each with the import's revision from
|
||||
the chunk before. A row the service refuses is listed with its reason and the others stay applied; sending the same
|
||||
file again applies nothing twice. To replace a product that exists, give its revision, or the row is refused as a
|
||||
conflict.
|
||||
|
||||
**A document is made from a file the project holds.** `archive.document.register` and `archive.version.add` take a
|
||||
`fileId` of the files module. The hash, size and type are the ones the store measured, the service keeps a Blob
|
||||
reference that names the file and nothing else of it, and a file of another project or organization is simply not
|
||||
there. A disposed document changes no more. The files module does not know a file is archived: deleting the file
|
||||
there removes the bytes a version points at, so an app that archives should not let members delete those files.
|
||||
|
||||
**A person can be linked to a member.** "This is me" on the party form stores the member's subject reference
|
||||
(`actorRef(bindingId, memberId)` of `@conusai-kit/business`) as an identifier of scheme
|
||||
`conusai.subject`. It must match the gateway's attributed app actor. The partition-aware privacy
|
||||
worker must resolve that exact member and binding; an unsupported callback leaves the obligation
|
||||
pending and cannot certify erasure.
|
||||
|
||||
## Your own codes and fields on a party
|
||||
|
||||
A party can carry what the service has no field for, and your own code for it.
|
||||
|
||||
- **The credential is scoped to an immutable app/environment binding.** The server resolves current
|
||||
app membership and permission before preparing the request. The gateway validates the deployment
|
||||
credential, binding and attributed actor. Rotating a credential does not remap retained data.
|
||||
- **Use the binding's namespace.** An extension uses `app_` followed by the binding UUID's 32 lower-case
|
||||
hexadecimal digits. The service validates that namespace against the signed partition. Request
|
||||
`crm.extensions:write` separately and obtain an explicit grant before defining a field; module
|
||||
inclusion does not grant this administrative scope. Existing app fields need a reviewed adoption
|
||||
plan; do not rename or copy customer records automatically.
|
||||
- **Your code is an identifier.** A scheme such as `app.<binding namespace>.party_code` belongs to that
|
||||
partition. Its code is unique among that partition's active parties. Use exact source values;
|
||||
another app tenant or environment has independent uniqueness and cannot adopt this namespace.
|
||||
- **A person you store is a reference.** Use `actorRef(bindingId, resolvedMemberId)` for an attributed
|
||||
app member, subject to the service's typed field and subject rules. Raw names, addresses and user IDs
|
||||
in arbitrary extension text need explicit subject handling; they cannot be inferred safely by an
|
||||
erasure worker.
|
||||
|
||||
Importing a file takes `crm.parties:import`, reading its ledger `crm.imports:read`, and roles
|
||||
`crm.roles:write`.
|
||||
|
||||
## Where the data is, and who sees it
|
||||
|
||||
The serving path requires the current server-prepared `RequestBusinessBinding`. It includes the
|
||||
immutable binding, app tenant, environment and verified member attribution. A missing or unverified
|
||||
binding is refused before network dispatch; no organization-wide environment fallback or deployment
|
||||
singleton is created. The gateway and service must enforce the same tuple under nonowner database
|
||||
roles before an app flow can be accepted. Explicit test ports and synthetic readiness observations
|
||||
are isolated test fixtures, not proof of production tenant isolation. Existing service data is
|
||||
retained; legacy organization-only rows are not silently reassigned to an app partition.
|
||||
|
||||
CRM parties of kind person and their contact points are personal data, kept and erased by the platform's CRM
|
||||
service; the app stores none of it. An archived document's title and its versions are the Archive service's. The
|
||||
module registers a document with no subject, so the platform's export or erasure of a person does not reach it: do
|
||||
not put a person's name in a title unless your app also records them as the document's subject.
|
||||
|
||||
## How a call is made
|
||||
|
||||
Each business module declares `transport: "business"` explicitly. Native Blob, OCR, AI and
|
||||
Knowledge requirements declare `transport: "platform"` and use their own observer and scope grammar.
|
||||
Existing apps must review and add this discriminator to their own requirements before adopting
|
||||
the matching kit release; package updates do not rewrite their module code or grant permissions.
|
||||
|
||||
Each module has a capability-limited port and a generated SDK adapter. Without an injected test
|
||||
port, the adapter resolves the prepared request binding from the authenticated dispatch's child
|
||||
container. It pins the credential, binding and actor, refuses another origin and redirects, and
|
||||
preserves the SDK's exact int64 codec. A `read` procedure receives only read operations. A manifest
|
||||
states supported API versions, profiles and exact per-operation scopes; an unavailable observation,
|
||||
provider or dependency is a refusal rather than implicit readiness.
|
||||
|
||||
The app composition creates these hooks lazily: modules that remain off need no extra environment
|
||||
metadata, and a missing business-service configuration refuses only the affected dispatch.
|
||||
`@conusai-kit/business` is a runtime dependency, including after production dependency pruning.
|
||||
|
||||
A refusal of the service becomes a typed error through the kit, with no mapping in the module: the adapter throws
|
||||
`businessAppError(toBusinessError(error, response))` of `@conusai-kit/business`. An admission refusal is kit-core's
|
||||
`service_<reason>` (below), which the page shows in place of itself with `serviceNotice(error,
|
||||
businessServiceNotices(i18n.locale))`. Every other refusal is `business_<refusal>` with the service's status:
|
||||
`business_not_found` (404), `business_invalid` (400), `business_conflict` and the service's own reasons
|
||||
(`business_insufficient_stock`, `business_unbalanced`, `business_legal_hold`, ...; 409), and
|
||||
`business_unavailable` (503) when the service did not answer. A form shows the sentence for any of them with
|
||||
`serviceNotice(error, labels.refusals)?.text`, where the loader put `businessRefusalNotices(i18n.locale)` (one
|
||||
sentence per refusal code in LT and EN, a module's own wording passed as overrides) in the labels. The admission codes:
|
||||
|
||||
| Code | Status | When |
|
||||
|---|---|---|
|
||||
| `service_not_installed`, `service_not_enabled` | 404 | Install the service or intentionally enable it for the organization. |
|
||||
| `service_permission_missing`, `service_not_entitled`, `service_partition_revoked` | 403 | Review the binding's approved scopes, entitlement or revocation with an administrator. |
|
||||
| `service_partition_missing`, `service_incompatible`, `service_profile_unsupported` | 409 | Configure the exact app/environment binding or review the supported release/profile. |
|
||||
| `service_configuration_missing` | 503 | Complete the required provider/service configuration. |
|
||||
| `service_dependency_unready`, `service_migrating`, `service_degraded` | 503 | Inspect the dependency, migration or health diagnostics. |
|
||||
| `service_observation_missing`, `service_observation_stale`, `service_generation_stale`, `service_signature_rejected` | 503 | Refresh verified status or ask the operator to reconcile the release. |
|
||||
|
||||
Remote error details are discarded, and a problem type a service does not answer with is `about:blank`; the UI
|
||||
uses the kit's bounded translated sentences. Amounts, quantities and dates are exact values of
|
||||
`@conusai-kit/money` and are shown through `i18n.money`, `i18n.quantity` and `i18n.day`
|
||||
([docs/how-to/use-money-and-refusals-in-an-app.md](../../../docs/how-to/use-money-and-refusals-in-an-app.md)).
|
||||
|
||||
## Testing
|
||||
|
||||
`fakeBusinessServices({ inventory, crm })` of `@conusai-kit/testing` simulates the gateway and services as a
|
||||
`fetch`: bind a real client to it (`businessClient(env).setConfig({ fetch: fake.fetch })`) and the port to the
|
||||
client (`sdkInventoryService`, `sdkCrmService`). `modules/<id>/__tests__/app.ts` does exactly that; the
|
||||
modules' tests drive every procedure, the not-enabled and missing-scope states, a viewer's refused write, and
|
||||
the app membership isolation suite. Injected ports do not establish external partition enforcement.
|
||||
Activities additionally tests generated wire encoding, prefixed resource IDs, opaque cursors, exact
|
||||
revision digits, comment receipts and preservation of untouched fields using synthetic HTTP.
|
||||
Real gateway/service/runtime-role/browser/API/MCP acceptance remains mandatory and separate.
|
||||
|
||||
`pnpm dev` runs against the fake platform, which serves the business gateway and the services the way the
|
||||
real gateway does: binding lookup, the partition admission of each operation, and forwarding to a fake of
|
||||
CRM, Catalog, Inventory, Archive or Activities in the binding's own partition. The seed binds the demo
|
||||
organizations with the scopes the selected modules declare; an organization created later shows
|
||||
`service_partition_missing` until it is bound, as in Cloud. `test/business-fake-platform.test.ts` runs a
|
||||
procedure through that path. Invoicing and Ledger have no fake: their pages show `service_not_installed`
|
||||
there. The platform's guide is `docs/how-to/test-business-services-locally.md` in the conusai-cloud repository.
|
||||
|
||||
## Invoicing adoption
|
||||
|
||||
`kit module:on invoicing` adds the optional organization-scoped invoice client. It requests only
|
||||
`invoicing.invoices:read`, `invoicing.invoices:write` and `invoicing.series:read`. An administrator
|
||||
must intentionally approve `invoicing.invoices:issue`, `invoicing.invoices:void` or
|
||||
`invoicing.series:write` before the corresponding fiscal/configuration controls can succeed.
|
||||
The app's fiscal actions also require its administrator role. Credit uses invoice-write scope
|
||||
but remains an app administrator operation. No privacy/delete scope is requested by inclusion.
|
||||
|
||||
Configure active numbering series and actual seller/buyer terms before drafting. CRM references
|
||||
are non-authorizing links; no CRM service access is inferred. Invoice data belongs to the app
|
||||
organization, including when its page is opened from a project. Existing applications adopt the
|
||||
new module and updated package pins explicitly, reviewing their own code; an update does not
|
||||
replace their modules, grant new scopes, enable the service or send invoices automatically.
|
||||
Issued data and numbering survive module disablement and require service-owned correction,
|
||||
retention and recovery procedures. See [the module](../modules/invoicing/README.md).
|
||||
|
||||
## Optional inclusion and retained data
|
||||
|
||||
The shipped starter selects Agent (including the assistant), Guides and Support and records all
|
||||
three in `everAdopted`. Documents, Knowledge, Activities, Archive, Catalog, CRM, Inventory,
|
||||
Invoicing and Ledger start unselected and never adopted. Physical source stays available. A
|
||||
separately authored new app may begin with empty selection and adoption lists; that valid source
|
||||
contract is not the shipped default or a reset mode for an existing app. From the app root,
|
||||
explicitly select or stop a module:
|
||||
|
||||
```sh
|
||||
pnpm kit module:on documents
|
||||
pnpm kit module:off documents
|
||||
```
|
||||
|
||||
The commands update `modules/inclusion.json` and deployment scope requests atomically. Review the
|
||||
diff, regenerate OpenAPI with `pnpm openapi`, run the app gates and deploy the reviewed revision.
|
||||
Selection requests only that module's operation scopes. It does not approve permissions, enable a
|
||||
Cloud organization/service, configure a provider or authorize costs. Configure those separately.
|
||||
The shipped starter already declares the native Guides bundle. When enabling Guides from an off
|
||||
configuration, publication needs a separate explicit deployment change (`guides.bundle` and the
|
||||
publishing service setting); selecting Guides alone does not restore publication. Support channel clients are
|
||||
lazy. Agent still needs independent model/tool policy and approval.
|
||||
|
||||
`selected` controls interactive/processing admission; `everAdopted` records custody under the
|
||||
original module identity and never shrinks automatically. Off keeps original schema/data and
|
||||
account-rights hooks, Knowledge removal and Agent/assistant retention. It keeps existing scope
|
||||
requests, including broad grants that cleanup currently needs, and adds no scope. It does not
|
||||
revoke an already approved credential. Do not delete data, migrations, the custody record or
|
||||
cleanup credentials merely because selection is off. Scope narrowing/removal requires separately
|
||||
verified obligation settlement and supported privacy APIs.
|
||||
|
||||
Old apps without this versioned record refuse automated toggling. Review their existing module
|
||||
composition, scope ownership, data and migrations, then explicitly adopt both the new composition
|
||||
and accurate selected/ever-adopted lists on a review branch. Do not seed an empty custody list for
|
||||
an app that previously used a module. Ordinary package/template upgrade does not perform this
|
||||
sensitive adoption. Convex remains preview; its separate composition needs coordinated adoption
|
||||
and unsupported module selection remains refused.
|
||||
|
||||
`module:off guides` also removes the top-level bundle declaration and per-service Guides publish
|
||||
roles from every deployment configuration, preserving unrelated fields and formatting. Re-enable
|
||||
requests runtime scopes again but does not restore publishing: opt in to publication separately.
|
||||
|
||||
## Human-triggered business jobs
|
||||
|
||||
Declare the job's exact service scopes on its owning module's `serviceRequirements`, and configure
|
||||
`createPostgresWorker` with `businessJobIntentPermissions: { "example.dispatch":
|
||||
["example.dispatch.send"] }`. Permissions come from trusted worker composition, not payloads or
|
||||
business service scopes. The Postgres worker owns the business job's retained claim, prepares its
|
||||
binding and admits current grants before invoking the handler. These handlers do not call
|
||||
`ctx.execution.run` again. The binding remains available through `requestBusinessBinding`.
|
||||
|
||||
Business clients recheck the original member's current intent, partition, grants, claim and real
|
||||
transaction fence before every request, and combine request cancellation with claim loss. For a
|
||||
separate mail/provider adapter, immediately before sending call
|
||||
`assertClaimedJobBusinessAdmission(ctx.container)` from `@conusai-kit/business/server` and use
|
||||
`AbortSignal.any([ctx.execution.signal, timeoutSignal])`. Keep stable idempotency keys and retries;
|
||||
already-started effects cannot be undone. Raw `fetch` calls are not automatically guarded.
|
||||
|
||||
Only project-scoped human-triggered Postgres business jobs are supported. Missing original users,
|
||||
revoked membership/permissions/grants/bindings, foreign scope, unsupported profile and missing or
|
||||
stale claim/fence refuse. Native jobs without business declarations keep their existing admission
|
||||
and execution contract. See the [canonical API and proof limits](../../../docs/reference/app-kit.md#claimed-business-jobs-on-postgres).
|
||||
|
||||
### Source-unit variant verification
|
||||
|
||||
From the platform checkout, run
|
||||
`node scripts/vendor/test-template-module-variants.mjs templates/app /absolute/sdk/node_modules /new/evidence/directory`
|
||||
with a compatible installed SDK/kit dependency tree. The helper constructs a separate new
|
||||
never-adopted source fixture with base-only permission requests before writing any adoption record;
|
||||
it does not erase an existing app's history. It executes the original admission, help/client and
|
||||
provider-construction assertions, including invalid-record and accidental-provider negative controls.
|
||||
Both fixture deployment configurations must pass the original generated platform-schema assertion.
|
||||
Each child suite has a two-minute deadline; timeout is a failed run, with no automatic retry.
|
||||
A second adopted fixture uses the public `kit module:off guides` command and verifies that Guides
|
||||
publication disappears across deployment profiles while adoption history remains unchanged.
|
||||
These are source-unit configuration checks with synthetic provider ports, not browser, database
|
||||
or deployed-platform acceptance. The shipped starter has no command to reset adoption history.
|
||||
Reference in New Issue
Block a user