@intelligo-dev/core 1.0.0-beta.13 → 1.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,13 @@
22
22
  "when": 1789808201382,
23
23
  "tag": "0002_webhook_claim_lease",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1790179158533,
30
+ "tag": "0003_local_payments",
31
+ "breakpoints": true
25
32
  }
26
33
  ]
27
34
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Plans, subscriptions, credit ledgers, purchases and Stripe events.
3
- * Amounts in the credit ledgers are micros of their currency.
2
+ * Plans, subscriptions, credit ledgers, purchases, local payments and
3
+ * Stripe events. Amounts in the credit ledgers are micros of their
4
+ * currency; amounts a buyer paid are minor units of theirs.
4
5
  */
5
6
 
6
7
  import {
@@ -13,7 +14,7 @@ import {
13
14
  index,
14
15
  unique,
15
16
  } from "drizzle-orm/pg-core";
16
- import { organization } from "./auth";
17
+ import { organization, users } from "./auth";
17
18
 
18
19
  /**
19
20
  * The registered plan catalogue, copied into rows so a subscription can
@@ -135,6 +136,39 @@ export const creditPurchases = pgTable(
135
136
  (table) => [index("credit_purchases_workspace_id_idx").on(table.workspaceId)]
136
137
  );
137
138
 
139
+ /**
140
+ * Invoices issued through a registered payment provider (QR-and-poll
141
+ * methods such as QPay, PIX or UPI), one row per invoice. The row is
142
+ * written when the invoice is opened, so settlement knows who pays and
143
+ * what was priced without trusting the browser; `fulfilled_at` is set
144
+ * in the same transaction that grants what was bought, so a payment is
145
+ * granted once however many polls see it paid.
146
+ */
147
+ export const payments = pgTable(
148
+ "payments",
149
+ {
150
+ id: text("id").primaryKey(),
151
+ /** The PAYMENT_MODE the invoice was opened under; settlement asks the same provider. */
152
+ provider: text("provider").notNull(),
153
+ invoiceId: text("invoice_id").notNull().unique(),
154
+ workspaceId: text("workspace_id")
155
+ .notNull()
156
+ .references(() => organization.id, { onDelete: "cascade" }),
157
+ userId: text("user_id").references(() => users.id, {
158
+ onDelete: "set null",
159
+ }),
160
+ /** What was bought, in the product's words: a plan slug, a bundle id. */
161
+ reference: text("reference").notNull(),
162
+ /** The price, in minor units of `currency`. */
163
+ amountMinor: integer("amount_minor").notNull(),
164
+ currency: text("currency").notNull(),
165
+ status: text("status").notNull().default("pending"), // pending|paid|failed
166
+ fulfilledAt: timestamp("fulfilled_at"),
167
+ createdAt: timestamp("created_at").notNull().defaultNow(),
168
+ },
169
+ (table) => [index("payments_workspace_id_idx").on(table.workspaceId)]
170
+ );
171
+
138
172
  /** Every Stripe event, for idempotent webhook processing and audit. */
139
173
  export const financeEvents = pgTable("finance_events", {
140
174
  id: text("id").primaryKey(),
@@ -201,6 +235,8 @@ export type CreditBalance = typeof creditBalances.$inferSelect;
201
235
  export type InsertCreditBalance = typeof creditBalances.$inferInsert;
202
236
  export type CreditPurchase = typeof creditPurchases.$inferSelect;
203
237
  export type InsertCreditPurchase = typeof creditPurchases.$inferInsert;
238
+ export type Payment = typeof payments.$inferSelect;
239
+ export type InsertPayment = typeof payments.$inferInsert;
204
240
  export type FinanceEvent = typeof financeEvents.$inferSelect;
205
241
  export type InsertFinanceEvent = typeof financeEvents.$inferInsert;
206
242
  export type BillingSettings = typeof billingSettings.$inferSelect;
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Privacy-facing reads and mutations over the user identity graph: fact
3
- * listing and deletion, full-identity export, and the memory-audit trail.
3
+ * listing and deletion, the profile snapshot's write, full-identity
4
+ * export, and the memory-audit trail.
4
5
  */
5
6
 
6
7
  export {
7
8
  listFacts,
8
9
  deleteFact,
10
+ saveProfileSnapshot,
9
11
  exportIdentity,
10
12
  getAuditTrail,
11
13
  } from "./service";
@@ -19,6 +21,7 @@ export {
19
21
  export type {
20
22
  IdentityActor,
21
23
  IdentityExport,
24
+ SaveProfileSnapshotInput,
22
25
  UserFact,
23
26
  UserMemory,
24
27
  UserProfileSnapshot,
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Server-only reads and mutations over the user identity graph: fact
3
- * listing and deletion, a full data export, and the memory-audit trail.
3
+ * listing and deletion, the profile snapshot's write, a full data export,
4
+ * and the memory-audit trail.
4
5
  *
5
6
  * `deleteFact` does not touch the cached profile snapshot; profile synthesis
6
- * is the product's AI code, so a caller that owns it re-triggers it.
7
+ * is the product's AI code, so a caller that owns it re-triggers it and
8
+ * stores the result with `saveProfileSnapshot`.
7
9
  *
8
10
  * Callers pass a resolved actor (workspaceId, userId); core cannot depend on
9
11
  * `@intelligo-dev/auth`. Every query filters by both ids. Every mutation
10
12
  * writes a `user_memory_audit` row. Failures throw `IdentityServiceError`.
11
13
  */
12
14
 
13
- import { and, desc, eq } from "drizzle-orm";
15
+ import { and, desc, eq, sql } from "drizzle-orm";
14
16
  import { db } from "../db";
15
17
  import {
16
18
  userFacts,
@@ -18,10 +20,18 @@ import {
18
20
  userProfileSnapshots,
19
21
  userMemoryAudit,
20
22
  } from "../db/schema";
21
- import type { UserFact, UserMemoryAuditRow } from "../db/schema";
23
+ import type {
24
+ UserFact,
25
+ UserMemoryAuditRow,
26
+ UserProfileSnapshot,
27
+ } from "../db/schema";
22
28
  import { recordMemoryAudit } from "./audit";
23
29
  import { IdentityServiceError } from "./errors";
24
- import type { IdentityActor, IdentityExport } from "./types";
30
+ import type {
31
+ IdentityActor,
32
+ IdentityExport,
33
+ SaveProfileSnapshotInput,
34
+ } from "./types";
25
35
 
26
36
  /**
27
37
  * Returns the fact if the actor owns it. Throws
@@ -200,3 +210,94 @@ export async function exportIdentity(
200
210
  audit,
201
211
  };
202
212
  }
213
+
214
+ /**
215
+ * Store the actor's synthesized profile — one row per user per workspace,
216
+ * inserted on the first synthesis and replaced on every later one. The
217
+ * version is bumped in the same statement (`ON CONFLICT … version + 1`),
218
+ * so two syntheses racing each other get two versions rather than one
219
+ * overwriting the other's number; it starts at 1.
220
+ *
221
+ * A field left `undefined` keeps what the row holds (null on insert); an
222
+ * explicit `null` clears it. `synthesizedAt` defaults to now. Records an
223
+ * audit row (`snapshot`, `create` or `update`) in the same transaction,
224
+ * attributed to `input.actorKind` — `system_job` unless the caller says
225
+ * otherwise. Throws `invalid_input` for an empty summary.
226
+ */
227
+ export async function saveProfileSnapshot(
228
+ actor: IdentityActor,
229
+ input: SaveProfileSnapshotInput
230
+ ): Promise<UserProfileSnapshot> {
231
+ if (!input.summary || input.summary.trim().length === 0) {
232
+ throw new IdentityServiceError("invalid_input", "summary is required");
233
+ }
234
+ if (input.factsDigest === undefined || input.factsDigest === null) {
235
+ throw new IdentityServiceError("invalid_input", "factsDigest is required");
236
+ }
237
+
238
+ const replaced = {
239
+ summary: input.summary,
240
+ factsDigest: input.factsDigest,
241
+ synthesizedAt: input.synthesizedAt ?? new Date(),
242
+ ...definedOnly({
243
+ summaryEn: input.summaryEn,
244
+ summaryMn: input.summaryMn,
245
+ activeGoals: input.activeGoals,
246
+ personalitySignals: input.personalitySignals,
247
+ relationshipNotes: input.relationshipNotes,
248
+ synthesizedByModel: input.synthesizedByModel,
249
+ nextSynthesisAt: input.nextSynthesisAt,
250
+ triggerReason: input.triggerReason,
251
+ }),
252
+ };
253
+
254
+ return db.transaction(async (tx) => {
255
+ const [row] = await tx
256
+ .insert(userProfileSnapshots)
257
+ .values({
258
+ userId: actor.userId,
259
+ workspaceId: actor.workspaceId,
260
+ version: 1,
261
+ ...replaced,
262
+ })
263
+ .onConflictDoUpdate({
264
+ target: [userProfileSnapshots.userId, userProfileSnapshots.workspaceId],
265
+ set: {
266
+ ...replaced,
267
+ version: sql`${userProfileSnapshots.version} + 1`,
268
+ },
269
+ })
270
+ .returning();
271
+
272
+ if (!row) {
273
+ throw new IdentityServiceError(
274
+ "database_error",
275
+ "Failed to save profile snapshot"
276
+ );
277
+ }
278
+
279
+ await recordMemoryAudit(
280
+ {
281
+ userId: actor.userId,
282
+ workspaceId: actor.workspaceId,
283
+ targetKind: "snapshot",
284
+ targetId: actor.userId,
285
+ action: row.version === 1 ? "create" : "update",
286
+ actorKind: input.actorKind ?? "system_job",
287
+ actorId: input.actorId,
288
+ afterValue: { version: row.version, triggerReason: row.triggerReason },
289
+ reason: input.reason,
290
+ },
291
+ tx
292
+ );
293
+
294
+ return row;
295
+ });
296
+ }
297
+
298
+ /** The entries whose value is not `undefined` — `null` is kept, to clear. */
299
+ function definedOnly<T extends Record<string, unknown>>(fields: T): Partial<T> {
300
+ return Object.fromEntries(
301
+ Object.entries(fields).filter(([, value]) => value !== undefined)
302
+ ) as Partial<T>;
303
+ }
@@ -1,4 +1,6 @@
1
1
  import type {
2
+ AuditActorKind,
3
+ SynthesisTriggerReason,
2
4
  UserFact,
3
5
  UserMemory,
4
6
  UserProfileSnapshot,
@@ -26,3 +28,31 @@ export type IdentityExport = {
26
28
  snapshot: UserProfileSnapshot | null;
27
29
  audit: UserMemoryAuditRow[];
28
30
  };
31
+
32
+ /**
33
+ * A synthesized profile, as `saveProfileSnapshot` stores it. The actor
34
+ * supplies the tenancy; the version is the store's.
35
+ */
36
+ export type SaveProfileSnapshotInput = {
37
+ /** The summary a prompt hydrates from, in the product's primary language. */
38
+ summary: string;
39
+ summaryEn?: string | null;
40
+ summaryMn?: string | null;
41
+ /** The structured digest the summary was written from. */
42
+ factsDigest: unknown;
43
+ activeGoals?: unknown;
44
+ personalitySignals?: unknown;
45
+ relationshipNotes?: unknown;
46
+ /** The model that wrote it, or a label for a deterministic synthesis. */
47
+ synthesizedByModel?: string | null;
48
+ /** Default: now. */
49
+ synthesizedAt?: Date;
50
+ /** When a scheduled re-synthesis may next pick this user up. */
51
+ nextSynthesisAt?: Date | null;
52
+ triggerReason?: SynthesisTriggerReason | null;
53
+ /** Who the audit row names. Default `"system_job"`. */
54
+ actorKind?: AuditActorKind;
55
+ actorId?: string;
56
+ /** Recorded on the audit row. */
57
+ reason?: string;
58
+ };
@@ -8,7 +8,11 @@ import { notifications } from "../db/schema";
8
8
  import { eq, and, desc, count } from "drizzle-orm";
9
9
  import type { CreateNotificationParams } from "./types";
10
10
 
11
- export type { NotificationType, CreateNotificationParams } from "./types";
11
+ export type {
12
+ BuiltInNotificationType,
13
+ NotificationType,
14
+ CreateNotificationParams,
15
+ } from "./types";
12
16
 
13
17
  export {
14
18
  triggerQuotaNotification,
@@ -1,4 +1,5 @@
1
- export type NotificationType =
1
+ /** The notification types the framework itself creates. */
2
+ export type BuiltInNotificationType =
2
3
  | "quota_warning_80"
3
4
  | "quota_warning_100"
4
5
  | "trial_warning_20"
@@ -8,6 +9,13 @@ export type NotificationType =
8
9
  | "subscription_confirmed"
9
10
  | "workspace_invitation";
10
11
 
12
+ /**
13
+ * A notification's `type`: one of the built-in types, or any string a
14
+ * product defines for its own notifications. `(string & {})` keeps the
15
+ * built-in names offered by autocompletion.
16
+ */
17
+ export type NotificationType = BuiltInNotificationType | (string & {});
18
+
11
19
  export interface CreateNotificationParams {
12
20
  userId: string;
13
21
  workspaceId?: string;