From bbc7a7f8f955682d87f5a1e47b4dab0da71a495b Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Thu, 7 Sep 2023 16:59:20 +0200 Subject: [PATCH] restore invoices --- apps/invoices/package.json | 1 - .../scripts/migrations/migration-utils.ts | 41 ++++++++ .../scripts/migrations/v1-to-v2/const.ts | 4 + .../scripts/migrations/v1-to-v2/readme.MD | 3 + .../migrations/v1-to-v2/restore-migration.ts | 46 +++++++++ .../migrations/v1-to-v2/run-migration.ts | 67 +++++++++++++ .../scripts/migrations/v1-to-v2/run-report.ts | 61 ++++++++++++ apps/invoices/src/fixtures/mock-address.ts | 4 +- .../app-configuration-router.ts | 5 +- .../schema-v1/app-config-v1.ts | 23 +++++ .../schema-v1/app-configurator.ts | 46 +++++++++ .../app-configuration/schema-v2/app-config.ts | 2 +- .../config-v1-to-v2-migration.service.test.ts | 95 +++++++++++++++++++ .../config-v1-to-v2-migration.service.ts | 57 +++++++++++ .../config-v1-to-v2-transformer.test.ts | 72 ++++++++++++++ .../schema-v2/config-v1-to-v2-transformer.ts | 29 ++++++ .../invoice-generator/invoice-generator.ts | 4 +- .../microinvoice-invoice-generator.ts | 44 +-------- .../pages/api/webhooks/invoice-requested.ts | 23 +++-- cspell.json | 4 +- 20 files changed, 574 insertions(+), 57 deletions(-) create mode 100644 apps/invoices/scripts/migrations/migration-utils.ts create mode 100644 apps/invoices/scripts/migrations/v1-to-v2/const.ts create mode 100644 apps/invoices/scripts/migrations/v1-to-v2/readme.MD create mode 100644 apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts create mode 100644 apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts create mode 100644 apps/invoices/scripts/migrations/v1-to-v2/run-report.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v1/app-config-v1.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v1/app-configurator.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.test.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.test.ts create mode 100644 apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.ts diff --git a/apps/invoices/package.json b/apps/invoices/package.json index 34a518b..8febb94 100644 --- a/apps/invoices/package.json +++ b/apps/invoices/package.json @@ -50,7 +50,6 @@ "@graphql-codegen/typescript-operations": "4.0.1", "@graphql-codegen/typescript-urql": "3.7.3", "@graphql-typed-document-node/core": "3.2.0", - "@total-typescript/ts-reset": "^0.5.1", "@types/react": "18.2.5", "@types/react-dom": "18.2.5", "@types/rimraf": "^3.0.2", diff --git a/apps/invoices/scripts/migrations/migration-utils.ts b/apps/invoices/scripts/migrations/migration-utils.ts new file mode 100644 index 0000000..d24106e --- /dev/null +++ b/apps/invoices/scripts/migrations/migration-utils.ts @@ -0,0 +1,41 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import { createGraphQLClient } from "@saleor/apps-shared"; +import { createSettingsManager } from "../../src/modules/app-configuration/metadata-manager"; +import { SaleorCloudAPL } from "@saleor/app-sdk/APL"; + +export const getMetadataManagerForEnv = (apiUrl: string, appToken: string) => { + const client = createGraphQLClient({ + saleorApiUrl: apiUrl, + token: appToken, + }); + + return createSettingsManager(client); +}; + +export const safeParse = (json?: string) => { + if (!json) return null; + + try { + return JSON.parse(json); + } catch (e) { + return null; + } +}; + +export const verifyRequiredEnvs = () => { + const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL", "SECRET_KEY"]; + + if (!requiredEnvs.every((env) => process.env[env])) { + throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`); + } +}; + +export const fetchCloudAplEnvs = () => { + const saleorAPL = new SaleorCloudAPL({ + token: process.env.SALEOR_CLOUD_TOKEN!, + resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!, + }); + + return saleorAPL.getAll(); +}; diff --git a/apps/invoices/scripts/migrations/v1-to-v2/const.ts b/apps/invoices/scripts/migrations/v1-to-v2/const.ts new file mode 100644 index 0000000..6488ecf --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/const.ts @@ -0,0 +1,4 @@ +export const MigrationV1toV2Consts = { + appConfigV2metadataKey: "app-config-v2", + appConfigV1metadataKey: "app-config", +}; diff --git a/apps/invoices/scripts/migrations/v1-to-v2/readme.MD b/apps/invoices/scripts/migrations/v1-to-v2/readme.MD new file mode 100644 index 0000000..8bc8029 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/readme.MD @@ -0,0 +1,3 @@ +Run `npx tsx run-report.ts` to print report (dry-run) +Run `npx tsx run-migration.ts` to migrate +Run `npx tsx restore-migration.ts` to restore migration (remove metadata v2) \ No newline at end of file diff --git a/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts b/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts new file mode 100644 index 0000000..97cadc4 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts @@ -0,0 +1,46 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import * as dotenv from "dotenv"; +import { fetchCloudAplEnvs, verifyRequiredEnvs } from "../migration-utils"; +import { RemoveMetadataDocument } from "../../../generated/graphql"; +import { MigrationV1toV2Consts } from "./const"; +import { createGraphQLClient } from "@saleor/apps-shared"; + +dotenv.config(); + +const runMigration = async () => { + verifyRequiredEnvs(); + + const allEnvs = await fetchCloudAplEnvs(); + + const results = await Promise.all( + allEnvs.map((env) => { + const client = createGraphQLClient({ + saleorApiUrl: env.saleorApiUrl, + token: env.token, + }); + + return client + .mutation(RemoveMetadataDocument, { + id: env.appId, + keys: [MigrationV1toV2Consts.appConfigV2metadataKey], + }) + .toPromise() + .then((r) => { + if (r.error) { + console.error("❌ Error removing metadata", r.error.message); + throw r.error.message; + } + + return r; + }) + .catch((e) => { + console.error("❌ Error removing metadata", e); + }); + }), + ); + + console.log(results); +}; + +runMigration(); diff --git a/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts b/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts new file mode 100644 index 0000000..25af362 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts @@ -0,0 +1,67 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import * as dotenv from "dotenv"; +import { + fetchCloudAplEnvs, + getMetadataManagerForEnv, + safeParse, + verifyRequiredEnvs, +} from "../migration-utils"; +import { ConfigV1ToV2Transformer } from "../../../src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer"; +import { AppConfigV2MetadataManager } from "../../../src/modules/app-configuration/schema-v2/app-config-v2-metadata-manager"; +import { AppConfigV2 } from "../../../src/modules/app-configuration/schema-v2/app-config"; +import { MigrationV1toV2Consts } from "./const"; + +dotenv.config(); + +const runMigration = async () => { + verifyRequiredEnvs(); + + const allEnvs = await fetchCloudAplEnvs(); + + const results = await Promise.all( + allEnvs.map((env) => { + const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token); + + return Promise.all([ + metadataManager.get(MigrationV1toV2Consts.appConfigV1metadataKey, env.saleorApiUrl), + metadataManager.get(MigrationV1toV2Consts.appConfigV2metadataKey), + ]) + .then(([v1, v2]) => { + if (v2 && v2 !== "undefined") { + console.log("▶️ v2 already exists for ", env.saleorApiUrl); + return; + } + + if (!v1) { + console.log("🚫 v1 does not exist for ", env.saleorApiUrl); + + return new AppConfigV2MetadataManager(metadataManager) + .set(new AppConfigV2().serialize()) + .then((r) => { + console.log(`✅ created empty config for ${env.saleorApiUrl}`); + }) + .catch((e) => { + console.log( + `🚫 failed to create empty config for ${env.saleorApiUrl}. Env may not exist.`, + e.message, + ); + }); + } + + const v2Config = new ConfigV1ToV2Transformer().transform(JSON.parse(v1)); + + return new AppConfigV2MetadataManager(metadataManager) + .set(v2Config.serialize()) + .then((r) => { + console.log(`✅ migrated ${env.saleorApiUrl}`); + }); + }) + .catch((e) => { + console.error("🚫 Failed to migrate ", env.saleorApiUrl, e); + }); + }), + ); +}; + +runMigration(); diff --git a/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts b/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts new file mode 100644 index 0000000..cb26555 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts @@ -0,0 +1,61 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import * as dotenv from "dotenv"; +import { + fetchCloudAplEnvs, + getMetadataManagerForEnv, + safeParse, + verifyRequiredEnvs, +} from "../migration-utils"; +import { MigrationV1toV2Consts } from "./const"; + +dotenv.config(); + +const runReport = async () => { + verifyRequiredEnvs(); + + const allEnvs = await fetchCloudAplEnvs().catch((r) => { + console.error(r); + + process.exit(1); + }); + + const results = await Promise.all( + allEnvs.map((env) => { + const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token); + + return Promise.all([ + metadataManager.get(MigrationV1toV2Consts.appConfigV1metadataKey, env.saleorApiUrl), + metadataManager.get(MigrationV1toV2Consts.appConfigV2metadataKey), + ]) + .then(([v1, v2]) => { + return { + schemaV1: safeParse(v1), + schemaV2: safeParse(v2), + }; + }) + .then((metadata) => ({ + metadata: metadata, + env: env.saleorApiUrl, + })); + }), + ); + + const report = results.map((r: any) => ({ + env: r.env, + hasV1: !!r.metadata.schemaV1, + hasV2: !!r.metadata.schemaV2, + })); + + const notMigratedCount = report.reduce((acc: number, curr: any) => { + if (!curr.hasV2) { + return acc + 1; + } + return acc; + }, 0); + + console.table(report); + console.log(`Envs left to migrate: ${notMigratedCount}`); +}; + +runReport(); diff --git a/apps/invoices/src/fixtures/mock-address.ts b/apps/invoices/src/fixtures/mock-address.ts index da03a81..cb6fdee 100644 --- a/apps/invoices/src/fixtures/mock-address.ts +++ b/apps/invoices/src/fixtures/mock-address.ts @@ -1,6 +1,6 @@ -import { ShopAddress } from "../modules/shop-info/shop-address"; +import { SellerShopConfig } from "../modules/app-configuration/schema-v1/app-config-v1"; -export const getMockAddress = (): ShopAddress => { +export const getMockAddress = (): SellerShopConfig["address"] => { return { city: "Wrocław", cityArea: "", diff --git a/apps/invoices/src/modules/app-configuration/app-configuration-router.ts b/apps/invoices/src/modules/app-configuration/app-configuration-router.ts index ec0a87d..05b6bf1 100644 --- a/apps/invoices/src/modules/app-configuration/app-configuration-router.ts +++ b/apps/invoices/src/modules/app-configuration/app-configuration-router.ts @@ -3,10 +3,11 @@ import { z } from "zod"; import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { router } from "../trpc/trpc-server"; import { createSettingsManager } from "./metadata-manager"; -import { AppConfigV2 } from "./schema-v2/app-config"; -import { AddressV2Schema } from "./schema-v2/app-config-schema.v2"; import { AppConfigV2MetadataManager } from "./schema-v2/app-config-v2-metadata-manager"; import { GetAppConfigurationV2Service } from "./schema-v2/get-app-configuration.v2.service"; +import { ConfigV1ToV2MigrationService } from "./schema-v2/config-v1-to-v2-migration.service"; +import { AddressV2Schema } from "./schema-v2/app-config-schema.v2"; +import { AppConfigV2 } from "./schema-v2/app-config"; const UpsertAddressSchema = z.object({ address: AddressV2Schema, diff --git a/apps/invoices/src/modules/app-configuration/schema-v1/app-config-v1.ts b/apps/invoices/src/modules/app-configuration/schema-v1/app-config-v1.ts new file mode 100644 index 0000000..c68a044 --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v1/app-config-v1.ts @@ -0,0 +1,23 @@ +import { SellerAddress } from "../address"; + +/** + * @deprecated + * Remove when SchemaV1 is migrated to SchemaV2 + */ +export interface SellerShopConfig { + address: SellerAddress; +} + +/** + * @deprecated + * Remove when SchemaV1 is migrated to SchemaV2 + */ +export type ShopConfigPerChannelSlug = Record; + +/** + * @deprecated + * Remove when SchemaV1 is migrated to SchemaV2 + */ +export type AppConfigV1 = { + shopConfigPerChannel: ShopConfigPerChannelSlug; +}; diff --git a/apps/invoices/src/modules/app-configuration/schema-v1/app-configurator.ts b/apps/invoices/src/modules/app-configuration/schema-v1/app-configurator.ts new file mode 100644 index 0000000..2eff00a --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v1/app-configurator.ts @@ -0,0 +1,46 @@ +import { AppConfigV1 } from "./app-config-v1"; +import { SettingsManager } from "@saleor/app-sdk/settings-manager"; + +/** + * @deprecated + * Remove when SchemaV1 is migrated to SchemaV2 + */ +export interface AppConfigurator { + setConfig(config: AppConfigV1): Promise; + getConfig(): Promise; +} + +/** + * @deprecated + * Remove when SchemaV1 is migrated to SchemaV2 + */ +export class PrivateMetadataAppConfiguratorV1 implements AppConfigurator { + private metadataKey = "app-config"; + + constructor( + private metadataManager: SettingsManager, + private saleorApiUrl: string, + ) {} + + getConfig(): Promise { + return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => { + if (!data) { + return data; + } + + try { + return JSON.parse(data); + } catch (e) { + throw new Error("Invalid metadata value, cant be parsed"); + } + }); + } + + setConfig(config: AppConfigV1): Promise { + return this.metadataManager.set({ + key: this.metadataKey, + value: JSON.stringify(config), + domain: this.saleorApiUrl, + }); + } +} diff --git a/apps/invoices/src/modules/app-configuration/schema-v2/app-config.ts b/apps/invoices/src/modules/app-configuration/schema-v2/app-config.ts index 5f12598..4218762 100644 --- a/apps/invoices/src/modules/app-configuration/schema-v2/app-config.ts +++ b/apps/invoices/src/modules/app-configuration/schema-v2/app-config.ts @@ -4,7 +4,7 @@ import { z } from "zod"; export class AppConfigV2 { private rootData: AppConfigV2Shape = { channelsOverrides: {} }; - constructor(initialData?: AppConfigV2Shape | unknown) { + constructor(initialData?: AppConfigV2Shape) { if (initialData) { this.rootData = AppConfigV2Schema.parse(initialData); } diff --git a/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.test.ts b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.test.ts new file mode 100644 index 0000000..27b2038 --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ConfigV1ToV2MigrationService } from "./config-v1-to-v2-migration.service"; +import { SimpleGraphqlClient } from "../metadata-manager"; +import { getMockAddress } from "../../../fixtures/mock-address"; +import { AppConfigV2 } from "./app-config"; + +describe("config-v1-to-v2-migration.service", () => { + const mockClient: SimpleGraphqlClient = { + mutation: vi.fn(), + query: vi.fn(), + }; + + let service: ConfigV1ToV2MigrationService; + + beforeEach(() => { + vi.resetAllMocks(); + + service = new ConfigV1ToV2MigrationService(mockClient, "https://example.com/graphql/"); + + vi.spyOn(service.configMetadataManager, "set").mockImplementationOnce(async () => + Promise.resolve(), + ); + }); + + it("Returns a pure V2 config if V1 config is not present", async () => { + vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () => + Promise.resolve(undefined), + ); + + const migrationResult = await service.migrate(); + + expect(migrationResult.getChannelsOverrides()).toEqual({}); + expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize()); + }); + + it("Returns a migrated V2 config from V1 if V1 config is present", async () => { + vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () => + Promise.resolve({ + shopConfigPerChannel: { + "default-channel": { + address: getMockAddress(), + }, + }, + }), + ); + + const migrationResult = await service.migrate(); + + expect(migrationResult.getChannelsOverrides()).toEqual( + expect.objectContaining({ + "default-channel": expect.objectContaining(getMockAddress()), + }), + ); + }); + + it("Runs a beforeSave callback and saves modified state in metadata - missing v1 config scenario", async () => { + vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () => + Promise.resolve(undefined), + ); + + const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => { + config.upsertOverride("test", getMockAddress()); + }); + + const migrationResult = await service.migrate(beforeSaveCb); + + expect(migrationResult.getChannelsOverrides()).toEqual({ + test: expect.objectContaining(getMockAddress()), + }); + expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize()); + expect(beforeSaveCb).toHaveBeenCalledWith(migrationResult); + }); + + it("Runs a beforeSave callback and saves modified state in metadata - present v1 config scenario", async () => { + vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () => + Promise.resolve({ + shopConfigPerChannel: { + "default-channel": { + address: getMockAddress(), + }, + }, + }), + ); + + const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => { + config.removeOverride("default-channel"); + }); + + const migrationResult = await service.migrate(beforeSaveCb); + + expect(migrationResult.getChannelsOverrides()).toEqual({}); + expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize()); + expect(beforeSaveCb).toHaveBeenCalledWith(migrationResult); + }); +}); diff --git a/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.ts b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.ts new file mode 100644 index 0000000..0390bc7 --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-migration.service.ts @@ -0,0 +1,57 @@ +import { PrivateMetadataAppConfiguratorV1 } from "../schema-v1/app-configurator"; +import { createSettingsManager, SimpleGraphqlClient } from "../metadata-manager"; +import { AppConfigV2 } from "./app-config"; +import { ConfigV1ToV2Transformer } from "./config-v1-to-v2-transformer"; +import { AppConfigV2MetadataManager } from "./app-config-v2-metadata-manager"; +import { SettingsManager } from "@saleor/app-sdk/settings-manager"; + +export class ConfigV1ToV2MigrationService { + settingsManager: SettingsManager; + configMetadataManager: AppConfigV2MetadataManager; + metadataV1AppConfigurator: PrivateMetadataAppConfiguratorV1; + + constructor( + private client: SimpleGraphqlClient, + private saleorApiUrl: string, + ) { + this.settingsManager = createSettingsManager(client); + this.configMetadataManager = new AppConfigV2MetadataManager(this.settingsManager); + this.metadataV1AppConfigurator = new PrivateMetadataAppConfiguratorV1( + this.settingsManager, + this.saleorApiUrl, + ); + } + + async migrate(beforeSave?: (config: AppConfigV2) => void): Promise { + const v1Config = await this.metadataV1AppConfigurator.getConfig(); + + /** + * If no v1 config, it means clean install - return pure config + */ + if (!v1Config) { + const pureConfig = new AppConfigV2(); + + if (beforeSave) { + beforeSave(pureConfig); + } + + await this.configMetadataManager.set(pureConfig.serialize()); + + return pureConfig; + } + + /** + * Otherwise, transform v1 config to v2 and save it + */ + const transformer = new ConfigV1ToV2Transformer(); + const appConfigV2FromV1 = transformer.transform(v1Config); + + if (beforeSave) { + beforeSave(appConfigV2FromV1); + } + + await this.configMetadataManager.set(appConfigV2FromV1.serialize()); + + return appConfigV2FromV1; + } +} diff --git a/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.test.ts b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.test.ts new file mode 100644 index 0000000..22901a8 --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { ConfigV1ToV2Transformer } from "./config-v1-to-v2-transformer"; +import { getMockAddress } from "../../../fixtures/mock-address"; + +describe("ConfigV1ToV2Transformer", function () { + it("Returns empty V2 instance if config is null", () => { + // @ts-expect-error + const v2 = new ConfigV1ToV2Transformer().transform(null); + + expect(v2.serialize()).toMatchInlineSnapshot('"{\\"channelsOverrides\\":{}}"'); + }); + + it("Maps V1 address overrides to V2 - single channel override", () => { + const v2 = new ConfigV1ToV2Transformer().transform({ + shopConfigPerChannel: { + "default-channel": { + address: getMockAddress(), + }, + }, + }); + + expect(v2.getChannelsOverrides()).toEqual( + expect.objectContaining({ + "default-channel": getMockAddress(), + }), + ); + }); + + it("Maps V1 address overrides to V2 - multiple channels override", () => { + const v2 = new ConfigV1ToV2Transformer().transform({ + shopConfigPerChannel: { + "default-channel": { + address: getMockAddress(), + }, + "custom-channel": { + address: getMockAddress(), + }, + }, + }); + + expect(v2.getChannelsOverrides()).toEqual( + expect.objectContaining({ + "default-channel": getMockAddress(), + "custom-channel": getMockAddress(), + }), + ); + }); + + it("Falls back to empty string for address property if not set", () => { + const addressMock = getMockAddress(); + + // @ts-expect-error + delete addressMock.city; + + const v2 = new ConfigV1ToV2Transformer().transform({ + shopConfigPerChannel: { + "default-channel": { + address: addressMock, + }, + }, + }); + + expect(v2.getChannelsOverrides()).toEqual( + expect.objectContaining({ + "default-channel": { + ...getMockAddress(), + city: "", + }, + }), + ); + }); +}); diff --git a/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.ts b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.ts new file mode 100644 index 0000000..af7e9bc --- /dev/null +++ b/apps/invoices/src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer.ts @@ -0,0 +1,29 @@ +import { AppConfigV1 } from "../schema-v1/app-config-v1"; +import { AppConfigV2 } from "./app-config"; + +export class ConfigV1ToV2Transformer { + transform(v1Config: AppConfigV1): AppConfigV2 { + const configV2 = new AppConfigV2(); + + if (!v1Config || !v1Config.shopConfigPerChannel) { + return configV2; + } + + Object.entries(v1Config.shopConfigPerChannel).forEach(([channelSlug, channelConfigV1]) => { + const addressV1 = channelConfigV1.address; + + configV2.upsertOverride(channelSlug, { + city: addressV1.city ?? "", + country: addressV1.country ?? "", + streetAddress2: addressV1.streetAddress2 ?? "", + postalCode: addressV1.postalCode ?? "", + companyName: addressV1.companyName ?? "", + streetAddress1: addressV1.streetAddress1 ?? "", + countryArea: addressV1.countryArea ?? "", + cityArea: addressV1.cityArea ?? "", + }); + }); + + return configV2; + } +} diff --git a/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts b/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts index d1467f0..377b01d 100644 --- a/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts +++ b/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts @@ -1,11 +1,11 @@ import { OrderPayloadFragment } from "../../../../generated/graphql"; -import { ShopAddress } from "../../shop-info/shop-address"; +import { SellerShopConfig } from "../../app-configuration/schema-v1/app-config-v1"; export interface InvoiceGenerator { generate(input: { order: OrderPayloadFragment; invoiceNumber: string; filename: string; - companyAddressData: ShopAddress; + companyAddressData: SellerShopConfig["address"]; }): Promise; } diff --git a/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts index db55476..0161b9d 100644 --- a/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts +++ b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts @@ -1,6 +1,7 @@ -import { OrderPayloadFragment } from "../../../../../generated/graphql"; -import { AddressV2Shape } from "../../../app-configuration/schema-v2/app-config-schema.v2"; import { InvoiceGenerator } from "../invoice-generator"; +import { Order, OrderPayloadFragment } from "../../../../../generated/graphql"; +import { SellerShopConfig } from "../../../app-configuration/schema-v1/app-config-v1"; +import { AddressV2Shape } from "../../../app-configuration/schema-v2/app-config-schema.v2"; const Microinvoice = require("microinvoice"); export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator { @@ -18,17 +19,7 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator { const { invoiceNumber, order, companyAddressData, filename } = input; const microinvoiceInstance = new Microinvoice({ - style: { - /* - * header: { - * image: { - * path: "./examples/logo.png", - * width: 50, - * height: 19, - * }, - * }, - */ - }, + style: {}, data: { invoice: { name: `Invoice ${invoiceNumber}`, @@ -62,12 +53,6 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator { order.billingAddress?.country.country, ], }, - /* - * { - * label: "Tax Identifier", - * value: "todo", - * }, - */ ], seller: [ @@ -83,28 +68,9 @@ export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator { companyAddressData.countryArea, ], }, - /* - * { - * label: "Tax Identifier", - * value: "todo", - * }, - */ ], - legal: [ - /* - * { - * value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit", - * weight: "bold", - * color: "primary", - * }, - * { - * value: "sed do eiusmod tempor incididunt ut labore et dolore magna.", - * weight: "bold", - * color: "secondary", - * }, - */ - ], + legal: [], details: { header: [ diff --git a/apps/invoices/src/pages/api/webhooks/invoice-requested.ts b/apps/invoices/src/pages/api/webhooks/invoice-requested.ts index ef0421a..dcee98c 100644 --- a/apps/invoices/src/pages/api/webhooks/invoice-requested.ts +++ b/apps/invoices/src/pages/api/webhooks/invoice-requested.ts @@ -1,25 +1,30 @@ -import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const"; import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; -import { createGraphQLClient, createLogger } from "@saleor/apps-shared"; import { gql } from "urql"; +import { saleorApp } from "../../../saleor-app"; import { InvoiceRequestedPayloadFragment, OrderPayloadFragment, } from "../../../../generated/graphql"; -import { AddressV2Shape } from "../../../modules/app-configuration/schema-v2/app-config-schema.v2"; -import { GetAppConfigurationV2Service } from "../../../modules/app-configuration/schema-v2/get-app-configuration.v2.service"; +import { SaleorInvoiceUploader } from "../../../modules/invoices/invoice-uploader/saleor-invoice-uploader"; import { InvoiceCreateNotifier } from "../../../modules/invoices/invoice-create-notifier/invoice-create-notifier"; -import { hashInvoiceFilename } from "../../../modules/invoices/invoice-file-name/hash-invoice-filename"; -import { resolveTempPdfFileLocation } from "../../../modules/invoices/invoice-file-name/resolve-temp-pdf-file-location"; -import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator"; import { InvoiceNumberGenerationStrategy, InvoiceNumberGenerator, } from "../../../modules/invoices/invoice-number-generator/invoice-number-generator"; -import { SaleorInvoiceUploader } from "../../../modules/invoices/invoice-uploader/saleor-invoice-uploader"; +import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator"; +import { hashInvoiceFilename } from "../../../modules/invoices/invoice-file-name/hash-invoice-filename"; +import { resolveTempPdfFileLocation } from "../../../modules/invoices/invoice-file-name/resolve-temp-pdf-file-location"; +import { createGraphQLClient, createLogger } from "@saleor/apps-shared"; +import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const"; +import { GetAppConfigurationV2Service } from "../../../modules/app-configuration/schema-v2/get-app-configuration.v2.service"; import { ShopInfoFetcher } from "../../../modules/shop-info/shop-info-fetcher"; +import { z } from "zod"; +import { + AddressV2Schema, + AddressV2Shape, +} from "../../../modules/app-configuration/schema-v2/app-config-schema.v2"; +import { ConfigV1ToV2MigrationService } from "../../../modules/app-configuration/schema-v2/config-v1-to-v2-migration.service"; import { shopInfoQueryToAddressShape } from "../../../modules/shop-info/shop-info-query-to-address-shape"; -import { saleorApp } from "../../../saleor-app"; import * as Sentry from "@sentry/nextjs"; import { AppConfigV2 } from "../../../modules/app-configuration/schema-v2/app-config"; diff --git a/cspell.json b/cspell.json index fc6df1c..9435751 100644 --- a/cspell.json +++ b/cspell.json @@ -67,6 +67,7 @@ "vals", "urql", "Protos", + "Consts", "pino", "IFRAME", "dedupe" @@ -82,6 +83,7 @@ "**/*.spec.ts", "**/graphql.ts", "**/CHANGELOG.md", - "**/schema.graphql" + "**/schema.graphql", + "**/*mock*" ] }