@porulle/core 0.7.0 → 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/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +26 -7
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -0
- package/dist/config/types.d.ts +26 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout-completion.d.ts.map +1 -1
- package/dist/hooks/checkout-completion.js +2 -2
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +17 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +206 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +97 -21
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +13 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +111 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +234 -1
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/database/scoped-db.d.ts +2 -1
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +62 -1
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +26 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
- package/dist/modules/analytics/drizzle-adapter.js +16 -3
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +61 -2
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +166 -5
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +391 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +56 -1
- package/dist/modules/orders/service.d.ts +102 -3
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +653 -29
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +13 -1
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +43 -5
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -0
- package/dist/test-utils/order-test-helpers.d.ts +12 -0
- package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
- package/dist/test-utils/order-test-helpers.js +18 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +7 -0
- package/dist/test-utils/test-actors.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/package.json +3 -3
- package/src/auth/middleware.ts +26 -7
- package/src/auth/setup.ts +11 -4
- package/src/config/defaults.ts +7 -0
- package/src/config/types.ts +26 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +36 -11
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +260 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +114 -27
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/inventory.ts +14 -0
- package/src/interfaces/rest/routes/orders.ts +148 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +258 -1
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/database/scoped-db.ts +70 -1
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +41 -2
- package/src/modules/analytics/drizzle-adapter.ts +19 -3
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +104 -2
- package/src/modules/catalog/entity-service.ts +196 -4
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +58 -1
- package/src/modules/orders/service.ts +1041 -65
- package/src/modules/orders/stale-order-cleanup.ts +18 -4
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +76 -6
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/create-test-config.ts +2 -0
- package/src/test-utils/order-test-helpers.ts +26 -0
- package/src/test-utils/rest-api-test-utils.ts +7 -0
- package/src/test-utils/test-actors.ts +1 -1
- package/src/testing.ts +1 -0
|
@@ -212,7 +212,7 @@ export class EntityService {
|
|
|
212
212
|
const afterHooks = this.deps.hooks.resolve("catalog.afterCreate") as CatalogCreateAfterHook[];
|
|
213
213
|
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
214
214
|
const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
|
|
215
|
-
const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, metadata: processedInput.metadata ?? {} }, ctx);
|
|
215
|
+
const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
|
|
216
216
|
if (processedInput.attributes) {
|
|
217
217
|
await this.repo.createAttribute({ entityId: entity.id, locale: processedInput.attributes.locale ?? "en", title: processedInput.attributes.title, subtitle: processedInput.attributes.subtitle, description: processedInput.attributes.description, richDescription: processedInput.attributes.richDescription, seoTitle: processedInput.attributes.seoTitle, seoDescription: processedInput.attributes.seoDescription }, ctx);
|
|
218
218
|
}
|
|
@@ -232,7 +232,7 @@ export class EntityService {
|
|
|
232
232
|
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
233
233
|
const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.update"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
|
|
234
234
|
const processed = await runBeforeHooks(beforeHooks, input, "update", context);
|
|
235
|
-
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, ctx);
|
|
235
|
+
const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, ctx);
|
|
236
236
|
if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
|
|
237
237
|
const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
|
|
238
238
|
const hydrated = await this.hydrateEntity(updated, undefined, ctx);
|
|
@@ -271,7 +271,12 @@ export class EntityService {
|
|
|
271
271
|
entity = refetched;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
|
|
274
|
+
const resolvedActor = actor ?? ctx?.actor ?? null;
|
|
275
|
+
try {
|
|
276
|
+
this.assertSameOrg(entity, resolvedActor);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return Err(toCommerceError(error));
|
|
279
|
+
}
|
|
275
280
|
const result = await this.hydrateEntity(entity, processed.options ?? options, ctx);
|
|
276
281
|
const entityAfterHooks = this.deps.hooks.resolve(`catalog.${entity.type}.afterRead`) as CatalogReadAfterHook[];
|
|
277
282
|
const report = mergeHookReports(await runAfterHooks(globalAfterHooks, null, result, "read", context), await runAfterHooks(entityAfterHooks, null, result, "read", context));
|
|
@@ -407,6 +412,11 @@ export class EntityService {
|
|
|
407
412
|
assertPermission(actor, "catalog:update");
|
|
408
413
|
const entity = await this.repo.findEntityById(input.entityId, ctx);
|
|
409
414
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
415
|
+
try {
|
|
416
|
+
this.assertSameOrg(entity, actor);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return Err(toCommerceError(error));
|
|
419
|
+
}
|
|
410
420
|
const optionType = await this.repo.createOptionType({ entityId: input.entityId, name: input.name, displayName: input.name, sortOrder: 0 }, ctx);
|
|
411
421
|
if (input.values) {
|
|
412
422
|
for (const value of input.values) {
|
|
@@ -420,6 +430,13 @@ export class EntityService {
|
|
|
420
430
|
assertPermission(actor, "catalog:update");
|
|
421
431
|
const optionType = await this.repo.findOptionTypeById(input.optionTypeId, ctx);
|
|
422
432
|
if (!optionType) return Err(new CommerceNotFoundError("Option type not found."));
|
|
433
|
+
const entity = await this.repo.findEntityById(optionType.entityId, ctx);
|
|
434
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
435
|
+
try {
|
|
436
|
+
this.assertSameOrg(entity, actor);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return Err(toCommerceError(error));
|
|
439
|
+
}
|
|
423
440
|
return Ok(await this.repo.createOptionValue({ optionTypeId: input.optionTypeId, value: input.value, displayValue: input.value, sortOrder: 0, metadata: {} }, ctx));
|
|
424
441
|
}
|
|
425
442
|
|
|
@@ -427,6 +444,11 @@ export class EntityService {
|
|
|
427
444
|
assertPermission(actor, "catalog:update");
|
|
428
445
|
const entity = await this.repo.findEntityById(input.entityId, ctx);
|
|
429
446
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
447
|
+
try {
|
|
448
|
+
this.assertSameOrg(entity, actor);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
return Err(toCommerceError(error));
|
|
451
|
+
}
|
|
430
452
|
const entityOptionTypes = await this.repo.findOptionTypesByEntityId(input.entityId, ctx);
|
|
431
453
|
const optionValueIds: string[] = [];
|
|
432
454
|
for (const [optName, optVal] of Object.entries(input.options)) {
|
|
@@ -437,7 +459,7 @@ export class EntityService {
|
|
|
437
459
|
if (!ov) return Err(new CommerceValidationError(`Option value "${optVal}" does not exist for option type "${optName}".`));
|
|
438
460
|
optionValueIds.push(ov.id);
|
|
439
461
|
}
|
|
440
|
-
const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}) }, ctx);
|
|
462
|
+
const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
|
|
441
463
|
await this.repo.createVariantOptionValues(optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })), ctx);
|
|
442
464
|
return Ok(variant);
|
|
443
465
|
}
|
|
@@ -461,6 +483,11 @@ export class EntityService {
|
|
|
461
483
|
}
|
|
462
484
|
const entity = await this.repo.findEntityById(entityId, ctx);
|
|
463
485
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
486
|
+
try {
|
|
487
|
+
this.assertSameOrg(entity, actor);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return Err(toCommerceError(error));
|
|
490
|
+
}
|
|
464
491
|
const entityOptionTypes = await this.repo.findOptionTypesByEntityId(entityId, ctx);
|
|
465
492
|
const sortedOptionTypes = entityOptionTypes.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
466
493
|
const optionValueGroups: string[][] = [];
|
|
@@ -490,4 +517,169 @@ export class EntityService {
|
|
|
490
517
|
}
|
|
491
518
|
return Ok(created);
|
|
492
519
|
}
|
|
520
|
+
|
|
521
|
+
// ── One-call variant creation (issue #50) ────────────────────────────────
|
|
522
|
+
// Upserts option axes inline, creates variants, and seeds a zero-stock
|
|
523
|
+
// inventory level per variant so it is sellable immediately. Retry-safe:
|
|
524
|
+
// axes upsert by name/value and existing combinations are skipped.
|
|
525
|
+
|
|
526
|
+
private async upsertAxes(
|
|
527
|
+
entityId: string,
|
|
528
|
+
axes: Array<{ name: string; values: string[] }>,
|
|
529
|
+
ctx?: TxContext,
|
|
530
|
+
): Promise<Map<string, Map<string, string>>> {
|
|
531
|
+
const existingTypes = await this.repo.findOptionTypesByEntityId(entityId, ctx);
|
|
532
|
+
const result = new Map<string, Map<string, string>>();
|
|
533
|
+
for (const axis of axes) {
|
|
534
|
+
let optionType = existingTypes.find((t) => t.name === axis.name);
|
|
535
|
+
if (!optionType) {
|
|
536
|
+
optionType = await this.repo.createOptionType(
|
|
537
|
+
{ entityId, name: axis.name, displayName: axis.name, sortOrder: existingTypes.length },
|
|
538
|
+
ctx,
|
|
539
|
+
);
|
|
540
|
+
existingTypes.push(optionType);
|
|
541
|
+
}
|
|
542
|
+
const existingValues = await this.repo.findOptionValuesByTypeId(optionType.id, ctx);
|
|
543
|
+
const valueIds = new Map<string, string>();
|
|
544
|
+
for (const value of axis.values) {
|
|
545
|
+
let optionValue = existingValues.find((v) => v.value === value);
|
|
546
|
+
if (!optionValue) {
|
|
547
|
+
optionValue = await this.repo.createOptionValue(
|
|
548
|
+
{ optionTypeId: optionType.id, value, displayValue: value, sortOrder: existingValues.length, metadata: {} },
|
|
549
|
+
ctx,
|
|
550
|
+
);
|
|
551
|
+
existingValues.push(optionValue);
|
|
552
|
+
}
|
|
553
|
+
valueIds.set(value, optionValue.id);
|
|
554
|
+
}
|
|
555
|
+
result.set(axis.name, valueIds);
|
|
556
|
+
}
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private async existingComboKeys(entityId: string, ctx?: TxContext): Promise<Map<string, string>> {
|
|
561
|
+
const existing = new Map<string, string>();
|
|
562
|
+
for (const variant of await this.repo.findVariantsByEntityId(entityId, ctx)) {
|
|
563
|
+
const rows = await this.repo.findVariantOptionValues(variant.id, ctx);
|
|
564
|
+
const key = rows.map((r) => r.optionValueId).sort().join("|");
|
|
565
|
+
existing.set(key, variant.id);
|
|
566
|
+
}
|
|
567
|
+
return existing;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private async seedInventoryLevel(
|
|
571
|
+
entityId: string,
|
|
572
|
+
variantId: string,
|
|
573
|
+
actor: Actor | null,
|
|
574
|
+
ctx?: TxContext,
|
|
575
|
+
): Promise<Result<unknown>> {
|
|
576
|
+
const inventory = this.deps.services.inventory as {
|
|
577
|
+
adjustDetailed: (
|
|
578
|
+
input: { entityId: string; variantId?: string; adjustment: number; reason: string },
|
|
579
|
+
actor?: Actor | null,
|
|
580
|
+
ctx?: TxContext,
|
|
581
|
+
) => Promise<Result<unknown>>;
|
|
582
|
+
};
|
|
583
|
+
return inventory.adjustDetailed(
|
|
584
|
+
{ entityId, variantId, adjustment: 0, reason: "Variant created — seed zero-stock level" },
|
|
585
|
+
actor,
|
|
586
|
+
ctx,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async quickCreateVariant(
|
|
591
|
+
entityId: string,
|
|
592
|
+
input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
|
|
593
|
+
actor: Actor | null,
|
|
594
|
+
ctx?: TxContext,
|
|
595
|
+
): Promise<Result<{ variant: Variant; created: boolean }>> {
|
|
596
|
+
try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
|
|
597
|
+
const entity = await this.repo.findEntityById(entityId, ctx);
|
|
598
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
599
|
+
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
600
|
+
|
|
601
|
+
const axes = Object.entries(input.options).map(([name, value]) => ({ name, values: [value] }));
|
|
602
|
+
const axisValueIds = await this.upsertAxes(entityId, axes, ctx);
|
|
603
|
+
const optionValueIds = Object.entries(input.options)
|
|
604
|
+
.map(([name, value]) => axisValueIds.get(name)!.get(value)!);
|
|
605
|
+
|
|
606
|
+
const key = [...optionValueIds].sort().join("|");
|
|
607
|
+
const existing = await this.existingComboKeys(entityId, ctx);
|
|
608
|
+
const existingVariantId = existing.get(key);
|
|
609
|
+
if (existingVariantId !== undefined) {
|
|
610
|
+
const variant = await this.repo.findVariantById(existingVariantId, ctx);
|
|
611
|
+
return Ok({ variant: variant!, created: false });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const variant = await this.repo.createVariant(
|
|
615
|
+
{
|
|
616
|
+
entityId,
|
|
617
|
+
status: "active",
|
|
618
|
+
sortOrder: 0,
|
|
619
|
+
metadata: { generatedBy: "quick" },
|
|
620
|
+
...(input.sku !== undefined ? { sku: input.sku } : {}),
|
|
621
|
+
...(input.barcode !== undefined ? { barcode: input.barcode } : {}),
|
|
622
|
+
},
|
|
623
|
+
ctx,
|
|
624
|
+
);
|
|
625
|
+
await this.repo.createVariantOptionValues(
|
|
626
|
+
optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })),
|
|
627
|
+
ctx,
|
|
628
|
+
);
|
|
629
|
+
const seeded = await this.seedInventoryLevel(entityId, variant.id, actor, ctx);
|
|
630
|
+
if (!seeded.ok) return seeded;
|
|
631
|
+
return Ok({ variant, created: true });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async bulkCreateVariants(
|
|
635
|
+
entityId: string,
|
|
636
|
+
input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
|
|
637
|
+
actor: Actor | null,
|
|
638
|
+
ctx?: TxContext,
|
|
639
|
+
): Promise<Result<{ created: Variant[]; skipped: number }>> {
|
|
640
|
+
try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
|
|
641
|
+
const entity = await this.repo.findEntityById(entityId, ctx);
|
|
642
|
+
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
643
|
+
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
644
|
+
|
|
645
|
+
const axisValueIds = await this.upsertAxes(entityId, input.axes, ctx);
|
|
646
|
+
// Empty axis list → one option-less variant.
|
|
647
|
+
const groups = input.axes.map((axis) =>
|
|
648
|
+
axis.values.map((value) => ({ value, id: axisValueIds.get(axis.name)!.get(value)! })),
|
|
649
|
+
);
|
|
650
|
+
const combinations = cartesian(groups);
|
|
651
|
+
|
|
652
|
+
const existing = await this.existingComboKeys(entityId, ctx);
|
|
653
|
+
const created: Variant[] = [];
|
|
654
|
+
let skipped = 0;
|
|
655
|
+
for (const combo of combinations) {
|
|
656
|
+
const key = combo.map((c) => c.id).sort().join("|");
|
|
657
|
+
if (existing.has(key)) {
|
|
658
|
+
skipped += 1;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const sku = input.skuPrefix !== undefined
|
|
662
|
+
? [input.skuPrefix, ...combo.map((c) => c.value)].join("-").toUpperCase().replace(/\s+/g, "-")
|
|
663
|
+
: undefined;
|
|
664
|
+
const variant = await this.repo.createVariant(
|
|
665
|
+
{
|
|
666
|
+
entityId,
|
|
667
|
+
status: "active",
|
|
668
|
+
sortOrder: 0,
|
|
669
|
+
metadata: { generatedBy: "bulk" },
|
|
670
|
+
...(sku !== undefined ? { sku } : {}),
|
|
671
|
+
},
|
|
672
|
+
ctx,
|
|
673
|
+
);
|
|
674
|
+
await this.repo.createVariantOptionValues(
|
|
675
|
+
combo.map((c) => ({ variantId: variant.id, optionValueId: c.id })),
|
|
676
|
+
ctx,
|
|
677
|
+
);
|
|
678
|
+
const seeded = await this.seedInventoryLevel(entityId, variant.id, actor, ctx);
|
|
679
|
+
if (!seeded.ok) return seeded;
|
|
680
|
+
existing.set(key, variant.id);
|
|
681
|
+
created.push(variant);
|
|
682
|
+
}
|
|
683
|
+
return Ok({ created, skipped });
|
|
684
|
+
}
|
|
493
685
|
}
|
|
@@ -25,6 +25,9 @@ export const sellableEntities = pgTable(
|
|
|
25
25
|
.notNull()
|
|
26
26
|
.default("draft"),
|
|
27
27
|
isVisible: boolean("is_visible").notNull().default(false),
|
|
28
|
+
// Tax class name (issue #57) — maps to tax_classes.name at checkout;
|
|
29
|
+
// a variant-level taxClass overrides this. Null = the org's default class.
|
|
30
|
+
taxClass: text("tax_class"),
|
|
28
31
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
|
|
29
32
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
30
33
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
@@ -183,6 +186,8 @@ export const variants = pgTable("variants", {
|
|
|
183
186
|
.notNull(),
|
|
184
187
|
sku: text("sku").unique(),
|
|
185
188
|
barcode: text("barcode"),
|
|
189
|
+
// Overrides the entity's taxClass when set (issue #57).
|
|
190
|
+
taxClass: text("tax_class"),
|
|
186
191
|
status: text("status", { enum: ["active", "discontinued"] })
|
|
187
192
|
.notNull()
|
|
188
193
|
.default("active"),
|
|
@@ -6,6 +6,7 @@ export const CreateEntityBodySchema = z.object({
|
|
|
6
6
|
type: z.string().openapi({ example: "physicalGood" }),
|
|
7
7
|
slug: z.string().openapi({ example: "my-product" }),
|
|
8
8
|
status: z.string().optional().openapi({ example: "draft" }),
|
|
9
|
+
taxClass: z.string().optional().openapi({ example: "standard", description: "Product tax class name (issue #57)." }),
|
|
9
10
|
basePrice: z.number().optional().openapi({ example: 29.99 }),
|
|
10
11
|
currency: z.string().optional().openapi({ example: "USD" }),
|
|
11
12
|
metadata: z.record(z.string(), z.unknown()).optional().openapi({ example: { title: "My Product" } }),
|
|
@@ -24,6 +25,7 @@ export const CreateEntityBodySchema = z.object({
|
|
|
24
25
|
export const UpdateEntityBodySchema = z.object({
|
|
25
26
|
slug: z.string().optional(),
|
|
26
27
|
status: z.string().optional(),
|
|
28
|
+
taxClass: z.string().nullable().optional(),
|
|
27
29
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
28
30
|
isVisible: z.boolean().optional(),
|
|
29
31
|
}).openapi("UpdateEntityBody");
|
|
@@ -69,6 +71,7 @@ export const CreateVariantBodySchema = z.object({
|
|
|
69
71
|
sku: z.string().optional().openapi({ example: "SKU-001" }),
|
|
70
72
|
options: z.record(z.string(), z.string()).openapi({ example: { Color: "Red" } }),
|
|
71
73
|
price: z.number().optional().openapi({ example: 34.99 }),
|
|
74
|
+
taxClass: z.string().optional().openapi({ example: "zero", description: "Overrides the entity's tax class (issue #57)." }),
|
|
72
75
|
}).openapi("CreateVariantBody");
|
|
73
76
|
|
|
74
77
|
const VariantMatrixRuleSchema = z.object({
|
|
@@ -259,6 +259,18 @@ export interface CatalogService {
|
|
|
259
259
|
actor: Actor | null,
|
|
260
260
|
ctx?: TxContext,
|
|
261
261
|
): Promise<Result<Variant[]>>;
|
|
262
|
+
quickCreateVariant(
|
|
263
|
+
entityId: string,
|
|
264
|
+
input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
|
|
265
|
+
actor: Actor | null,
|
|
266
|
+
ctx?: TxContext,
|
|
267
|
+
): Promise<Result<{ variant: Variant; created: boolean }>>;
|
|
268
|
+
bulkCreateVariants(
|
|
269
|
+
entityId: string,
|
|
270
|
+
input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
|
|
271
|
+
actor: Actor | null,
|
|
272
|
+
ctx?: TxContext,
|
|
273
|
+
): Promise<Result<{ created: Variant[]; skipped: number }>>;
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
export interface CatalogServiceDeps {
|
|
@@ -411,4 +423,22 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
411
423
|
generateVariants(entityId: string, strategy: VariantGenerationStrategy, actor: Actor | null, ctx?: TxContext): Promise<Result<Variant[]>> {
|
|
412
424
|
return this.entities.generateVariants(entityId, strategy, actor, ctx);
|
|
413
425
|
}
|
|
426
|
+
|
|
427
|
+
quickCreateVariant(
|
|
428
|
+
entityId: string,
|
|
429
|
+
input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
|
|
430
|
+
actor: Actor | null,
|
|
431
|
+
ctx?: TxContext,
|
|
432
|
+
): Promise<Result<{ variant: Variant; created: boolean }>> {
|
|
433
|
+
return this.entities.quickCreateVariant(entityId, input, actor, ctx);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
bulkCreateVariants(
|
|
437
|
+
entityId: string,
|
|
438
|
+
input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
|
|
439
|
+
actor: Actor | null,
|
|
440
|
+
ctx?: TxContext,
|
|
441
|
+
): Promise<Result<{ created: Variant[]; skipped: number }>> {
|
|
442
|
+
return this.entities.bulkCreateVariants(entityId, input, actor, ctx);
|
|
443
|
+
}
|
|
414
444
|
}
|
|
@@ -241,6 +241,12 @@ export class CustomerService {
|
|
|
241
241
|
ctx?: TxContext,
|
|
242
242
|
options?: { replaceMetadata?: boolean },
|
|
243
243
|
): Promise<Result<Customer>> {
|
|
244
|
+
try {
|
|
245
|
+
assertPermission(actor ?? null, "customers:update");
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return Err(toCommerceError(error));
|
|
248
|
+
}
|
|
249
|
+
|
|
244
250
|
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
245
251
|
const existing = await this.repo.findById(orgId, id, ctx);
|
|
246
252
|
if (!existing) return Err(new CommerceNotFoundError("Customer not found."));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
2
|
+
import { defineModule } from "../../kernel/module/index.js";
|
|
3
|
+
import { DocumentsRepository } from "./repository/index.js";
|
|
4
|
+
import { invoiceSequences, orderDocuments } from "./schema.js";
|
|
5
|
+
import { DocumentsService } from "./service.js";
|
|
6
|
+
|
|
7
|
+
export const documentsModule = defineModule<
|
|
8
|
+
{ invoiceSequences: typeof invoiceSequences; orderDocuments: typeof orderDocuments },
|
|
9
|
+
DocumentsService,
|
|
10
|
+
Record<string, never>
|
|
11
|
+
>({
|
|
12
|
+
id: "documents",
|
|
13
|
+
schema: () => ({ invoiceSequences, orderDocuments }),
|
|
14
|
+
service: (deps) =>
|
|
15
|
+
new DocumentsService({
|
|
16
|
+
repository: new DocumentsRepository(deps.db.db as DrizzleDatabase),
|
|
17
|
+
services: deps.services as Record<string, unknown>,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal dependency-free PDF writer (issue #47).
|
|
3
|
+
*
|
|
4
|
+
* Emits PDF 1.4 with Helvetica / Helvetica-Bold (built-in base-14 fonts, no
|
|
5
|
+
* embedding) and uncompressed content streams. Pure string/Uint8Array work —
|
|
6
|
+
* no Node-only APIs, so it runs on Cloudflare Workers and other serverless
|
|
7
|
+
* runtimes. Enough for text documents like invoices; not a general renderer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface PdfTextOp {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
size: number;
|
|
14
|
+
bold?: boolean | undefined;
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A4 in PDF points. */
|
|
19
|
+
export const A4 = { width: 595.28, height: 841.89 } as const;
|
|
20
|
+
|
|
21
|
+
// Escape to a PDF literal string: backslash, parens, and non-ASCII bytes as
|
|
22
|
+
// octal escapes (Latin-1 / WinAnsi range; anything beyond becomes "?").
|
|
23
|
+
function escapePdfText(text: string): string {
|
|
24
|
+
let out = "";
|
|
25
|
+
for (const ch of text) {
|
|
26
|
+
const code = ch.codePointAt(0)!;
|
|
27
|
+
if (ch === "\\" || ch === "(" || ch === ")") out += `\\${ch}`;
|
|
28
|
+
else if (code >= 32 && code <= 126) out += ch;
|
|
29
|
+
else if (code > 126 && code <= 255) out += `\\${code.toString(8).padStart(3, "0")}`;
|
|
30
|
+
else out += "?";
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function contentStream(ops: PdfTextOp[]): string {
|
|
36
|
+
const parts: string[] = [];
|
|
37
|
+
for (const op of ops) {
|
|
38
|
+
const font = op.bold ? "/F2" : "/F1";
|
|
39
|
+
parts.push(
|
|
40
|
+
`BT ${font} ${op.size} Tf ${op.x.toFixed(2)} ${op.y.toFixed(2)} Td (${escapePdfText(op.text)}) Tj ET`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return parts.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds a complete PDF document; one inner array of text ops per page.
|
|
48
|
+
*/
|
|
49
|
+
export function buildPdf(pages: PdfTextOp[][]): Uint8Array {
|
|
50
|
+
// Object numbering: 1 Catalog, 2 Pages, 3 F1, 4 F2, then per page: Page, Contents.
|
|
51
|
+
const objects: string[] = [];
|
|
52
|
+
const pageObjectNumbers: number[] = [];
|
|
53
|
+
const firstPageObj = 5;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < pages.length; i++) {
|
|
56
|
+
pageObjectNumbers.push(firstPageObj + i * 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
objects.push(`1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n`);
|
|
60
|
+
objects.push(
|
|
61
|
+
`2 0 obj\n<< /Type /Pages /Kids [${pageObjectNumbers.map((n) => `${n} 0 R`).join(" ")}] /Count ${pages.length} >>\nendobj\n`,
|
|
62
|
+
);
|
|
63
|
+
objects.push(`3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n`);
|
|
64
|
+
objects.push(`4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n`);
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < pages.length; i++) {
|
|
67
|
+
const pageNum = pageObjectNumbers[i]!;
|
|
68
|
+
const contentNum = pageNum + 1;
|
|
69
|
+
const stream = contentStream(pages[i]!);
|
|
70
|
+
objects.push(
|
|
71
|
+
`${pageNum} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${A4.width} ${A4.height}] ` +
|
|
72
|
+
`/Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents ${contentNum} 0 R >>\nendobj\n`,
|
|
73
|
+
);
|
|
74
|
+
objects.push(
|
|
75
|
+
`${contentNum} 0 obj\n<< /Length ${stream.length} >>\nstream\n${stream}\nendstream\nendobj\n`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const header = "%PDF-1.4\n";
|
|
80
|
+
let body = header;
|
|
81
|
+
const offsets: number[] = [];
|
|
82
|
+
for (const obj of objects) {
|
|
83
|
+
offsets.push(body.length);
|
|
84
|
+
body += obj;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const xrefOffset = body.length;
|
|
88
|
+
const count = objects.length + 1;
|
|
89
|
+
let xref = `xref\n0 ${count}\n0000000000 65535 f \n`;
|
|
90
|
+
for (const offset of offsets) {
|
|
91
|
+
xref += `${offset.toString().padStart(10, "0")} 00000 n \n`;
|
|
92
|
+
}
|
|
93
|
+
const trailer = `trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
|
|
94
|
+
|
|
95
|
+
const full = body + xref + trailer;
|
|
96
|
+
// All bytes are ASCII/Latin-1 by construction (escapePdfText), so a
|
|
97
|
+
// byte-per-char encoding is exact.
|
|
98
|
+
const bytes = new Uint8Array(full.length);
|
|
99
|
+
for (let i = 0; i < full.length; i++) bytes[i] = full.charCodeAt(i) & 0xff;
|
|
100
|
+
return bytes;
|
|
101
|
+
}
|