import assert from "node:assert"; import type { Address, Associate, BusinessUnit, BusinessUnitAddAddressAction, BusinessUnitAddAssociateAction, BusinessUnitAddBillingAddressIdAction, BusinessUnitAddShippingAddressIdAction, BusinessUnitAddStoreAction, BusinessUnitChangeAddressAction, BusinessUnitChangeApprovalRuleModeAction, BusinessUnitChangeAssociateAction, BusinessUnitChangeAssociateModeAction, BusinessUnitChangeNameAction, BusinessUnitChangeParentUnitAction, BusinessUnitChangeStatusAction, BusinessUnitDraft, BusinessUnitKeyReference, BusinessUnitRemoveAddressAction, BusinessUnitRemoveAssociateAction, BusinessUnitRemoveBillingAddressIdAction, BusinessUnitRemoveShippingAddressIdAction, BusinessUnitSetAddressCustomFieldAction, BusinessUnitSetAddressCustomTypeAction, BusinessUnitSetAssociatesAction, BusinessUnitSetContactEmailAction, BusinessUnitSetCustomFieldAction, BusinessUnitSetCustomTypeAction, BusinessUnitSetDefaultBillingAddressAction, BusinessUnitSetDefaultShippingAddressAction, BusinessUnitSetStoreModeAction, BusinessUnitUpdateAction, Company, CompanyDraft, Division, DivisionDraft, InvalidJsonInputError, InvalidOperationError, } from "@commercetools/platform-sdk"; import type { Config } from "#src/config.ts"; import { CommercetoolsError } from "#src/exceptions.ts"; import { BusinessUnitDraftSchema } from "#src/schemas/generated/business-unit.ts"; import { generateRandomString, getBaseResourceProperties } from "../helpers.ts"; import type { Writable } from "../types.ts"; import type { UpdateHandlerInterface } from "./abstract.ts"; import { AbstractResourceRepository, AbstractUpdateHandler, type RepositoryContext, } from "./abstract.ts"; import { createAddress, createAssociate, createCustomFields, getBusinessUnitKeyReference, getStoreKeyReference, } from "./helpers.ts"; export class BusinessUnitRepository extends AbstractResourceRepository<"business-unit"> { constructor(config: Config) { super("business-unit", config); this.actions = new BusinessUnitUpdateHandler(this._storage); this.draftSchema = BusinessUnitDraftSchema; } async create( context: RepositoryContext, draft: BusinessUnitDraft, ): Promise { const addresses = draft.addresses?.map((address) => ({ ...address, id: generateRandomString(5), })) ?? []; const defaultBillingAddressId = addresses.length > 0 && draft.defaultBillingAddress !== undefined ? addresses[draft.defaultBillingAddress].id : undefined; const defaultShippingAddressId = addresses.length > 0 && draft.defaultShippingAddress !== undefined ? addresses[draft.defaultShippingAddress].id : undefined; const shippingAddressIds = draft.shippingAddresses?.map( (i) => addresses[i].id, ); const billingAddressIds = draft.billingAddresses?.map( (i) => addresses[i].id, ); const stores = draft.stores ? await Promise.all( draft.stores.map((s) => getStoreKeyReference(s, context.projectKey, this._storage), ), ) : undefined; const associates = draft.associates ? await Promise.all( draft.associates.map((a) => createAssociate(a, context.projectKey, this._storage), ), ) : []; const resource = { ...getBaseResourceProperties(context.clientId), key: draft.key, status: draft.status, stores, storeMode: draft.storeMode, name: draft.name, contactEmail: draft.contactEmail, addresses: addresses, custom: await createCustomFields( draft.custom, context.projectKey, this._storage, ), shippingAddressIds: shippingAddressIds, billingAddressIds: billingAddressIds, defaultShippingAddressId: defaultShippingAddressId, defaultBillingAddressId: defaultBillingAddressId, associateMode: draft.associateMode, approvalRuleMode: draft.approvalRuleMode, associates, }; if (this._isDivisionDraft(draft)) { const parentUnit = await getBusinessUnitKeyReference( draft.parentUnit, context.projectKey, this._storage, ); // Look up the parent to determine the topLevelUnit const parent = (await this._storage.getByResourceIdentifier( context.projectKey, parentUnit, )) as BusinessUnit | undefined; const topLevelUnit: BusinessUnitKeyReference = parent?.topLevelUnit ?? { typeId: "business-unit", key: parentUnit.key!, }; const division = { ...resource, unitType: "Division" as const, parentUnit, topLevelUnit, } as Division; await this.saveNew(context, division); return division; } if (this._isCompanyDraft(draft)) { const company = { ...resource, unitType: "Company" as const, topLevelUnit: { typeId: "business-unit" as const, key: draft.key, }, } as Company; await this.saveNew(context, company); return company; } throw new CommercetoolsError( { code: "InvalidJsonInput", message: "Invalid business unit type", detailedErrorMessage: "Invalid business unit type", }, 400, ); } private _isCompanyDraft(draft: BusinessUnitDraft): draft is CompanyDraft { return draft.unitType === "Company"; } private _isDivisionDraft(draft: BusinessUnitDraft): draft is DivisionDraft { return draft.unitType === "Division"; } } class BusinessUnitUpdateHandler extends AbstractUpdateHandler implements Partial> { async addAddress( context: RepositoryContext, resource: Writable, { address }: BusinessUnitAddAddressAction, ) { const newAddress = createAddress( address, context.projectKey, this._storage, ); if (newAddress) { resource.addresses.push(newAddress); } } async addAssociate( context: RepositoryContext, resource: Writable, { associate }: BusinessUnitAddAssociateAction, ) { const newAssociate = await createAssociate( associate, context.projectKey, this._storage, ); if (newAssociate) { resource.associates.push(newAssociate); } } async addStore( context: RepositoryContext, resource: Writable, { store }: BusinessUnitAddStoreAction, ) { const newStore = await getStoreKeyReference( store, context.projectKey, this._storage, ); if (newStore) { if (!resource.stores) { resource.stores = []; } resource.stores.push(newStore); } } changeAddress( context: RepositoryContext, resource: Writable, { addressId, addressKey, address }: BusinessUnitChangeAddressAction, ) { const current = this._findAddress(resource, addressId, addressKey, true); assert(current?.id); // always true since we set required to true const oldAddressIndex = resource.addresses.findIndex( (a) => a.id === current.id, ); const newAddress = createAddress( { ...address, id: current.id }, context.projectKey, this._storage, ); if (newAddress) { resource.addresses[oldAddressIndex] = newAddress; } } changeApprovalRuleMode( context: RepositoryContext, resource: Writable, { approvalRuleMode }: BusinessUnitChangeApprovalRuleModeAction, ) { resource.approvalRuleMode = approvalRuleMode; } changeAssociateMode( context: RepositoryContext, resource: Writable, { associateMode }: BusinessUnitChangeAssociateModeAction, ) { resource.associateMode = associateMode; } changeName( context: RepositoryContext, resource: Writable, { name }: BusinessUnitChangeNameAction, ) { resource.name = name; } async changeParentUnit( context: RepositoryContext, resource: Writable, { parentUnit }: BusinessUnitChangeParentUnitAction, ) { resource.parentUnit = await getBusinessUnitKeyReference( parentUnit, context.projectKey, this._storage, ); } changeStatus( context: RepositoryContext, resource: Writable, { status }: BusinessUnitChangeStatusAction, ) { resource.status = status; } async setAssociates( context: RepositoryContext, resource: Writable, { associates }: BusinessUnitSetAssociatesAction, ) { const newAssociates = ( await Promise.all( associates.map((a) => createAssociate(a, context.projectKey, this._storage), ), ) ).filter((a): a is Writable => a !== undefined); resource.associates = newAssociates || undefined; } removeAssociate( context: RepositoryContext, resource: Writable, { customer }: BusinessUnitRemoveAssociateAction, ) { resource.associates = resource.associates.filter( (associate) => associate.customer.id !== customer.id, ); } async changeAssociate( context: RepositoryContext, resource: Writable, { associate }: BusinessUnitChangeAssociateAction, ) { const existingAssociateIndex = resource.associates.findIndex( (a) => a.customer.id === associate.customer.id, ); if (existingAssociateIndex === -1) { throw new CommercetoolsError( { code: "InvalidOperation", message: `Associate with customer id ${associate.customer.id} not found`, }, 400, ); } const newAssociate = await createAssociate( associate, context.projectKey, this._storage, ); if (newAssociate) { resource.associates[existingAssociateIndex] = newAssociate; } } setContactEmail( context: RepositoryContext, resource: Writable, { contactEmail }: BusinessUnitSetContactEmailAction, ) { resource.contactEmail = contactEmail; } async setCustomType( context: RepositoryContext, resource: Writable, { type, fields }: BusinessUnitSetCustomTypeAction, ) { await this._setCustomType(context, resource, { type, fields }); } setStoreMode( context: RepositoryContext, resource: Writable, { storeMode }: BusinessUnitSetStoreModeAction, ) { resource.storeMode = storeMode; } setDefaultShippingAddress( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitSetDefaultShippingAddressAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true resource.defaultShippingAddressId = address.id; if (resource.shippingAddressIds === undefined) { resource.shippingAddressIds = []; } if (!resource.shippingAddressIds.includes(address.id)) { resource.shippingAddressIds.push(address.id); } } addShippingAddressId( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitAddShippingAddressIdAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true if (resource.shippingAddressIds === undefined) { resource.shippingAddressIds = []; } if (!resource.shippingAddressIds.includes(address.id)) { resource.shippingAddressIds.push(address.id); } } removeShippingAddressId( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitRemoveShippingAddressIdAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true resource.shippingAddressIds = resource.shippingAddressIds?.filter( (id) => id !== address.id, ); if (resource.defaultShippingAddressId === address.id) { resource.defaultShippingAddressId = undefined; } } addBillingAddressId( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitAddBillingAddressIdAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true if (resource.billingAddressIds === undefined) { resource.billingAddressIds = []; } if (!resource.billingAddressIds.includes(address.id)) { resource.billingAddressIds.push(address.id); } } removeBillingAddressId( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitRemoveBillingAddressIdAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true resource.billingAddressIds = resource.billingAddressIds?.filter( (id) => id !== address.id, ); if (resource.defaultBillingAddressId === address.id) { resource.defaultBillingAddressId = undefined; } } setDefaultBillingAddress( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitSetDefaultBillingAddressAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true resource.defaultBillingAddressId = address.id; if (resource.billingAddressIds === undefined) { resource.billingAddressIds = []; } if (!resource.billingAddressIds.includes(address.id)) { resource.billingAddressIds.push(address.id); } } setCustomField( context: RepositoryContext, resource: Writable, { name, value }: BusinessUnitSetCustomFieldAction, ) { this._setCustomFieldValues(resource, { name, value }); } setAddressCustomField( context: RepositoryContext, resource: Writable, { addressId, name, value }: BusinessUnitSetAddressCustomFieldAction, ) { const address = resource.addresses.find((addr) => addr.id === addressId); if (!address) { throw new CommercetoolsError( { code: "InvalidOperation", message: `Address with id ${addressId} not found`, }, 400, ); } if (!address.custom) { // If the address doesn't have custom fields, we need to initialize them // This might require a type to be set first, but we'll just create minimal structure throw new CommercetoolsError( { code: "InvalidOperation", message: "Address has no custom type set. Use setAddressCustomType first.", }, 400, ); } address.custom.fields[name] = value; } async setAddressCustomType( context: RepositoryContext, resource: Writable, { addressId, type, fields }: BusinessUnitSetAddressCustomTypeAction, ) { const address = resource.addresses.find((addr) => addr.id === addressId); if (!address) { throw new CommercetoolsError( { code: "InvalidOperation", message: `Address with id ${addressId} not found`, }, 400, ); } if (!type) { address.custom = undefined; } else { address.custom = await createCustomFields( { type, fields }, context.projectKey, this._storage, ); } } removeAddress( context: RepositoryContext, resource: Writable, { addressId, addressKey }: BusinessUnitRemoveAddressAction, ) { const address = this._findAddress(resource, addressId, addressKey, true); assert(address?.id); // always true since we set required to true resource.addresses = resource.addresses.filter((a) => a.id !== address.id); if (resource.shippingAddressIds) { resource.shippingAddressIds = resource.shippingAddressIds.filter( (id) => id !== address.id, ); } if (resource.billingAddressIds) { resource.billingAddressIds = resource.billingAddressIds.filter( (id) => id !== address.id, ); } if (resource.defaultShippingAddressId === address.id) { resource.defaultShippingAddressId = undefined; } if (resource.defaultBillingAddressId === address.id) { resource.defaultBillingAddressId = undefined; } } private _findAddress( resource: Writable, addressId: string | undefined, addressKey: string | undefined, required = false, ): Address | undefined { if (addressKey) { const address = resource.addresses.find((a) => a.key === addressKey); if (!address) { throw new CommercetoolsError( { code: "InvalidOperation", message: `Business Unit does not contain an address with the key ${addressKey}.`, }, 400, ); } return address; } if (addressId) { const address = resource.addresses.find((a) => a.id === addressId); if (!address) { throw new CommercetoolsError( { code: "InvalidOperation", message: `Business Unit does not contain an address with the id ${addressId}.`, }, 400, ); } return address; } if (required) { throw new CommercetoolsError( { code: "InvalidOperation", message: "One of address 'addressId' or 'addressKey' is required.", }, 400, ); } } }