@porulle/plugin-channel-connector 0.9.0

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.
package/dist/schema.js ADDED
@@ -0,0 +1,104 @@
1
+ import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from "@porulle/core/drizzle";
2
+ export const connectedStores = pgTable("connected_stores", {
3
+ id: uuid("id").defaultRandom().primaryKey(),
4
+ organizationId: text("organization_id").notNull(),
5
+ provider: text("provider").notNull(),
6
+ credentials: jsonb("credentials").$type().notNull(),
7
+ storeDomain: text("store_domain").notNull(),
8
+ status: text("status", { enum: ["connected", "disconnected", "error"] })
9
+ .notNull()
10
+ .default("connected"),
11
+ catalogCursor: text("catalog_cursor"),
12
+ inventoryCursor: text("inventory_cursor"),
13
+ lastSyncAt: timestamp("last_sync_at", { withTimezone: true }),
14
+ lastReconcileAt: timestamp("last_reconcile_at", { withTimezone: true }),
15
+ lastReconcileReport: jsonb("last_reconcile_report").$type(),
16
+ webhookSecret: text("webhook_secret"),
17
+ breakerState: jsonb("breaker_state").$type().notNull().default({}),
18
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
19
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
20
+ }, (table) => ({
21
+ orgIdx: index("idx_connected_stores_org").on(table.organizationId),
22
+ orgProviderIdx: index("idx_connected_stores_org_provider").on(table.organizationId, table.provider),
23
+ }));
24
+ export const channelEntityMap = pgTable("channel_entity_map", {
25
+ id: uuid("id").defaultRandom().primaryKey(),
26
+ organizationId: text("organization_id").notNull(),
27
+ storeId: uuid("store_id").references(() => connectedStores.id, { onDelete: "cascade" }).notNull(),
28
+ kind: text("kind", { enum: ["entity", "variant"] }).notNull(),
29
+ externalId: text("external_id").notNull(),
30
+ entityId: uuid("entity_id").notNull(),
31
+ variantId: uuid("variant_id"),
32
+ lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).defaultNow().notNull(),
33
+ syncHash: text("sync_hash").notNull(),
34
+ }, (table) => ({
35
+ orgIdx: index("idx_channel_entity_map_org").on(table.organizationId),
36
+ storeIdx: index("idx_channel_entity_map_store").on(table.storeId),
37
+ externalUnique: uniqueIndex("channel_entity_map_store_kind_external_unique").on(table.storeId, table.kind, table.externalId),
38
+ }));
39
+ export const channelOrderExports = pgTable("channel_order_exports", {
40
+ id: uuid("id").defaultRandom().primaryKey(),
41
+ organizationId: text("organization_id").notNull(),
42
+ storeId: uuid("store_id").references(() => connectedStores.id, { onDelete: "cascade" }).notNull(),
43
+ orderId: uuid("order_id").notNull(),
44
+ customerData: jsonb("customer_data").$type(),
45
+ state: text("state", { enum: ["pending", "exported", "confirmed", "failed", "abandoned"] })
46
+ .notNull()
47
+ .default("pending"),
48
+ failureKind: text("failure_kind", { enum: ["definitive", "transient"] }),
49
+ remoteOrderId: text("remote_order_id"),
50
+ remoteUrl: text("remote_url"),
51
+ attempts: integer("attempts").notNull().default(0),
52
+ lastError: text("last_error"),
53
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
54
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
55
+ }, (table) => ({
56
+ orgIdx: index("idx_channel_order_exports_org").on(table.organizationId),
57
+ storeIdx: index("idx_channel_order_exports_store").on(table.storeId),
58
+ stateIdx: index("idx_channel_order_exports_state").on(table.organizationId, table.state),
59
+ orderIdx: index("idx_channel_order_exports_order").on(table.organizationId, table.orderId),
60
+ }));
61
+ export const channelExportEvents = pgTable("channel_export_events", {
62
+ id: uuid("id").defaultRandom().primaryKey(),
63
+ organizationId: text("organization_id").notNull(),
64
+ exportId: uuid("export_id")
65
+ .references(() => channelOrderExports.id, { onDelete: "cascade" })
66
+ .notNull(),
67
+ fromState: text("from_state").notNull(),
68
+ toState: text("to_state").notNull(),
69
+ reason: text("reason"),
70
+ changedBy: text("changed_by").notNull(),
71
+ changedAt: timestamp("changed_at", { withTimezone: true }).defaultNow().notNull(),
72
+ }, (table) => ({
73
+ orgIdx: index("idx_channel_export_events_org").on(table.organizationId),
74
+ exportIdx: index("idx_channel_export_events_export").on(table.exportId),
75
+ }));
76
+ export const channelRefundRequests = pgTable("channel_refund_requests", {
77
+ id: uuid("id").defaultRandom().primaryKey(),
78
+ organizationId: text("organization_id").notNull(),
79
+ storeId: uuid("store_id").references(() => connectedStores.id, { onDelete: "cascade" }).notNull(),
80
+ orderId: uuid("order_id").notNull(),
81
+ remoteRefundId: text("remote_refund_id").notNull(),
82
+ amount: integer("amount").notNull(),
83
+ state: text("state", { enum: ["requested", "approved", "rejected", "executed"] }).notNull().default("requested"),
84
+ approvedBy: text("approved_by"),
85
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
86
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
87
+ }, (table) => ({
88
+ orgIdx: index("idx_channel_refund_requests_org").on(table.organizationId),
89
+ pendingIdx: index("idx_channel_refund_requests_pending").on(table.organizationId, table.state),
90
+ remoteUnique: uniqueIndex("channel_refund_requests_store_remote_unique").on(table.storeId, table.remoteRefundId),
91
+ }));
92
+ export const channelRefundEvents = pgTable("channel_refund_events", {
93
+ id: uuid("id").defaultRandom().primaryKey(),
94
+ organizationId: text("organization_id").notNull(),
95
+ requestId: uuid("request_id").references(() => channelRefundRequests.id, { onDelete: "cascade" }).notNull(),
96
+ fromState: text("from_state"),
97
+ toState: text("to_state").notNull(),
98
+ reason: text("reason"),
99
+ changedBy: text("changed_by").notNull(),
100
+ changedAt: timestamp("changed_at", { withTimezone: true }).defaultNow().notNull(),
101
+ }, (table) => ({
102
+ orgIdx: index("idx_channel_refund_events_org").on(table.organizationId),
103
+ requestIdx: index("idx_channel_refund_events_request").on(table.requestId),
104
+ }));
@@ -0,0 +1,132 @@
1
+ import type { Actor, ChannelConnector, ChannelOrderSlice, PluginDb, PluginResult, PluginTxFn } from "@porulle/core";
2
+ import type { JobsAdapter } from "@porulle/core";
3
+ import { type ChannelOrderExport, type ChannelRefundRequest, type ConnectedStore } from "./schema.js";
4
+ export type ExportState = ChannelOrderExport["state"];
5
+ export interface ReconcileReport extends Record<string, unknown> {
6
+ imported: number;
7
+ converged: number;
8
+ archived: number;
9
+ inventoryUpdated: number;
10
+ driftAlert: boolean;
11
+ }
12
+ export type PublicConnectedStore = Omit<ConnectedStore, "credentials" | "webhookSecret"> & {
13
+ credentials: "[REDACTED]";
14
+ webhookSecret: "[REDACTED]";
15
+ };
16
+ export interface ChannelComplianceData {
17
+ customer: {
18
+ id?: string;
19
+ email?: string;
20
+ };
21
+ exports: Array<{
22
+ exportId: string;
23
+ orderId: string;
24
+ customerData: NonNullable<ChannelOrderExport["customerData"]>;
25
+ }>;
26
+ }
27
+ export interface ChannelConnectorPluginOptions {
28
+ connectors?: ChannelConnector[];
29
+ oauth?: {
30
+ stateSecret: string;
31
+ postConnectRedirect: string;
32
+ };
33
+ inventoryTimeoutMs?: number;
34
+ jobs?: JobsAdapter;
35
+ exportSla?: {
36
+ definitiveMs?: number;
37
+ transientMs?: number;
38
+ };
39
+ refundAutoMax?: number;
40
+ newStoreDays?: number;
41
+ driftAlertThreshold?: number;
42
+ reconcileJitterWindowMs?: number;
43
+ }
44
+ export interface ChannelStockLine {
45
+ entityId: string;
46
+ variantId?: string;
47
+ title?: string;
48
+ quantity: number;
49
+ }
50
+ export declare function canExportTransition(from: ExportState, to: ExportState): boolean;
51
+ export declare class ChannelConnectorService {
52
+ private readonly db;
53
+ private readonly services;
54
+ private readonly connectors;
55
+ private readonly transact;
56
+ private readonly jobs;
57
+ private readonly options;
58
+ constructor(db: PluginDb, services: Record<string, unknown>, options?: ChannelConnectorPluginOptions, transaction?: PluginTxFn);
59
+ getConnector(providerId: string): ChannelConnector | undefined;
60
+ private get catalog();
61
+ private getStoreRecord;
62
+ getStoreByDomain(shopDomain: string): Promise<ConnectedStore | undefined>;
63
+ getStoresByDomain(shopDomain: string): Promise<ConnectedStore[]>;
64
+ connectStore(orgId: string, input: {
65
+ provider: string;
66
+ credentials: Record<string, unknown>;
67
+ storeDomain: string;
68
+ webhookSecret?: string;
69
+ }): Promise<PluginResult<PublicConnectedStore>>;
70
+ private get optionsJobs();
71
+ disconnectStore(orgId: string, id: string): Promise<PluginResult<PublicConnectedStore>>;
72
+ disconnectStoreSystem(orgId: string, id: string, redactDomain?: boolean): Promise<PluginResult<PublicConnectedStore>>;
73
+ getStore(orgId: string, id: string): Promise<PluginResult<PublicConnectedStore>>;
74
+ listStores(orgId: string): Promise<PluginResult<PublicConnectedStore[]>>;
75
+ validateLineStock(orgId: string, lines: ChannelStockLine[], timeoutMs?: number): Promise<void>;
76
+ importCatalog(orgId: string, storeId: string, actor: Actor): Promise<PluginResult<{
77
+ imported: number;
78
+ cursor: string | null;
79
+ }>>;
80
+ private convergeCatalogItems;
81
+ reconcile(orgId: string, storeId: string, actor: Actor): Promise<PluginResult<ReconcileReport>>;
82
+ getReconcileStatus(orgId: string, storeId: string): Promise<PluginResult<{
83
+ lastReconcileAt: Date | null;
84
+ report: ReconcileReport | null;
85
+ driftAlert: boolean;
86
+ }>>;
87
+ syncInventory(orgId: string, storeId: string, actor: Actor): Promise<PluginResult<{
88
+ synced: number;
89
+ }>>;
90
+ handleWebhook(orgId: string, storeId: string, event: {
91
+ id: string;
92
+ type: string;
93
+ data: unknown;
94
+ }): Promise<PluginResult<{
95
+ processed: true;
96
+ data?: ChannelComplianceData;
97
+ redacted?: number;
98
+ }>>;
99
+ private complianceEmail;
100
+ private channelCustomerExports;
101
+ private channelCustomerDataRequest;
102
+ private redactCustomerData;
103
+ private redactShopData;
104
+ private resolveOrderId;
105
+ private setMappedInventory;
106
+ private convergeCatalogItem;
107
+ private createRefundRequest;
108
+ private executeRefund;
109
+ listRefundRequests(orgId: string): Promise<PluginResult<ChannelRefundRequest[]>>;
110
+ approveRefund(orgId: string, id: string, actor: {
111
+ userId: string;
112
+ }): Promise<PluginResult<ChannelRefundRequest>>;
113
+ rejectRefund(orgId: string, id: string, actor: {
114
+ userId: string;
115
+ }): Promise<PluginResult<ChannelRefundRequest>>;
116
+ private refundLinesForRequest;
117
+ createExport(orgId: string, storeId: string, orderId: string): Promise<PluginResult<ChannelOrderExport>>;
118
+ transitionExport(orgId: string, exportId: string, toState: ExportState, changedBy: string, reason?: string, failureKind?: "definitive" | "transient"): Promise<PluginResult<ChannelOrderExport>>;
119
+ exportOrder(orgId: string, storeId: string, slice: ChannelOrderSlice, actor: Actor): Promise<PluginResult<ChannelOrderExport>>;
120
+ buildOrderSlice(orgId: string, storeId: string, orderId: string): Promise<PluginResult<ChannelOrderSlice>>;
121
+ reapExports(input: {
122
+ definitiveMs: number;
123
+ transientMs: number;
124
+ }): Promise<{
125
+ abandonedCount: number;
126
+ refundedOrderIds: string[];
127
+ }>;
128
+ getExport(orgId: string, id: string): Promise<PluginResult<ChannelOrderExport>>;
129
+ listFailedExports(orgId: string): Promise<PluginResult<ChannelOrderExport[]>>;
130
+ retryExport(orgId: string, exportId: string, changedBy: string): Promise<PluginResult<ChannelOrderExport>>;
131
+ abandonExport(orgId: string, exportId: string, changedBy: string, reason?: string): Promise<PluginResult<ChannelOrderExport>>;
132
+ }