@devizovaburza/mdm-sdk 1.2.4 → 1.2.5-canary.061d760

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/v1/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { hc } from 'hono/client';
2
2
  import { z, createRoute, OpenAPIHono } from '@hono/zod-openapi';
3
3
  import { COUNTRY_CODES_2, LANGUAGE_CODES, CZ_NACE_CODES, CURRENCY_CODES, BANK_CODES, CZ_TRADE_LICENSE_CODES } from '@develit-io/general-codes';
4
- import { bankAccountMetadataSchema } from '@develit-io/backend-sdk';
4
+ import { bankAccountMetadataSchema, structuredAddressSchema } from '@develit-io/backend-sdk';
5
5
  import { jwt, signature, idempotency } from '@develit-io/backend-sdk/middlewares';
6
6
 
7
7
  const createMdmClient = (baseUrl, options) => {
@@ -194,6 +194,19 @@ const ID_DOC_STATUS = [
194
194
  "REVOKED"
195
195
  ];
196
196
  const CUSTOMER_STATUS = ["NEW", "ACTIVE", "CLOSED", "BLOCKED"];
197
+ const BLOCK_REASON = [
198
+ "MISSING_OWNERSHIP_STRUCTURE",
199
+ "REMOVED_FROM_REGISTRY",
200
+ "INSOLVENCY",
201
+ "EXECUTION",
202
+ "INACTIVE_FROM_START",
203
+ "INACTIVE_OVER_2_YEARS",
204
+ "COMPANY_SOLD",
205
+ "UNINTERESTING",
206
+ "OUTDATED_ID_DOCUMENTS",
207
+ "OTHER",
208
+ "BLOCKED_BY_CLIENT"
209
+ ];
197
210
  const PARTY_TYPE = [
198
211
  "INDIVIDUAL",
199
212
  "SELF_EMPLOYED",
@@ -310,6 +323,38 @@ const RISK_BUSINESS_AREA = [
310
323
  "VIRTUAL_ASSET_SERVICES"
311
324
  ];
312
325
 
326
+ function logAuditEvent(context, input) {
327
+ const identity = context.get("identity");
328
+ const sourceIp = context.req.header("CF-Connecting-IP");
329
+ const correlationId = input.correlationId ?? context.get("correlationId");
330
+ if (!correlationId) {
331
+ throw new Error(
332
+ "logAuditEvent: missing correlationId \u2014 auditLog() middleware must be mounted before handlers that call logAuditEvent"
333
+ );
334
+ }
335
+ context.executionCtx.waitUntil(
336
+ context.env.ACTIVITY_SERVICE.logAuditEvent({
337
+ logType: "event",
338
+ service: "gateway",
339
+ eventType: input.eventType,
340
+ correlationId,
341
+ actorUserId: input.actorUserId ?? identity?.user?.id,
342
+ actorEmail: input.actorEmail ?? identity?.user?.email ?? "not-authorized@system.internal",
343
+ actorOrganizationId: input.actorOrganizationId ?? identity?.user?.organizationId,
344
+ targetType: input.targetType,
345
+ targetId: input.targetId,
346
+ outcome: input.outcome,
347
+ description: input.description,
348
+ severity: input.severity ?? "low",
349
+ details: input.details,
350
+ userAgent: context.req.header("User-Agent"),
351
+ ...sourceIp && { sourceIp }
352
+ }).catch((error) => {
353
+ console.error(`Failed to log audit event "${input.eventType}":`, error);
354
+ })
355
+ );
356
+ }
357
+
313
358
  z.object({
314
359
  message: z.string(),
315
360
  data: z.null(),
@@ -350,7 +395,10 @@ const partySchema$1 = z.object({
350
395
  id: z.uuid(),
351
396
  internalId: z.string().optional(),
352
397
  note: z.string().optional(),
353
- countryCode: z.enum(COUNTRY_CODES).optional()
398
+ countryCode: z.enum(COUNTRY_CODES).optional(),
399
+ customerStatus: z.enum(CUSTOMER_STATUS).optional(),
400
+ blockReason: z.enum(BLOCK_REASON).nullable().optional(),
401
+ blockReasonNote: z.string().trim().min(1).nullable().optional()
354
402
  });
355
403
  const idDocumentInputSchema$1 = z.object({
356
404
  idDocType: z.enum(ID_DOC_TYPE),
@@ -455,6 +503,14 @@ const createAddressInputSchema = z.object({
455
503
  countryCode: z.enum(COUNTRY_CODES, "St\xE1t je povinn\xFD"),
456
504
  ruianCode: z.string().optional()
457
505
  });
506
+ const ownerAddressInputSchema = createAddressInputSchema.extend({
507
+ addressType: z.enum(ADDRESS_TYPE).default("PERMANENT_ADDRESS"),
508
+ district: z.string().max(255).nullish(),
509
+ region: z.string().max(255).nullish()
510
+ });
511
+ const ownerIndividualInputSchema = individualInsertSchema.extend({
512
+ email: z.email("E-mail je povinn\xFD").optional()
513
+ });
458
514
  const businessPartnerInputSchema$1 = z.object({
459
515
  name: z.string().min(1, "N\xE1zev partnera je povinn\xFD"),
460
516
  country: z.enum(COUNTRY_CODES, "St\xE1t partnera je povinn\xFD"),
@@ -523,7 +579,7 @@ const createAmlInputSchema = z.object({
523
579
  personName: z.string(),
524
580
  personRole: z.enum(PERSON_ROLE),
525
581
  answer: z.enum(YES_NO_UNKNOWN),
526
- country: z.enum(COUNTRY_CODES).nullable()
582
+ countries: z.array(z.enum(COUNTRY_CODES))
527
583
  })
528
584
  ).optional()
529
585
  });
@@ -536,19 +592,11 @@ const productsInputSchema$1 = z.object({
536
592
  }).optional(),
537
593
  cbs: z.object({}).optional()
538
594
  });
539
- const ownerInputSchema = z.object({
595
+ const ownerInputSchema = ownerIndividualInputSchema.extend({
540
596
  titleBefore: z.string().optional(),
541
- name: z.string().min(1, "Jm\xE9no je povinn\xE9"),
542
- surname: z.string().min(1, "P\u0159\xEDjmen\xED je povinn\xE9"),
543
597
  titleAfter: z.string().optional(),
544
- email: z.email("E-mail je povinn\xFD"),
545
- birthDate: z.string().min(1, "Datum narozen\xED je povinn\xE9"),
546
- birthPlace: z.string().optional(),
547
- personalId: z.string().min(1, "Rodn\xE9 \u010D\xEDslo je povinn\xE9"),
548
- gender: z.enum(GENDER, "Pohlav\xED je povinn\xE9"),
549
- citizenship: z.enum(COUNTRY_CODES, "St\xE1tn\xED p\u0159\xEDslu\u0161nost je povinn\xE1"),
550
598
  isPep: z.boolean({ error: "Politicky exponovan\xE1 osoba je povinn\xE1" }),
551
- address: createAddressInputSchema,
599
+ address: ownerAddressInputSchema,
552
600
  sharePercentage: z.number({ error: "Pod\xEDl na spole\u010Dnosti je povinn\xFD" }).min(1, "Pod\xEDl na spole\u010Dnosti je povinn\xFD").max(100),
553
601
  shareEstablishedAt: z.string().min(1, "Datum vzniku pod\xEDlu je povinn\xFD")
554
602
  });
@@ -567,7 +615,7 @@ const legalRepresentativeInputSchema = z.object({
567
615
  phone: z.string().optional(),
568
616
  pin: z.string().optional(),
569
617
  identityDocuments: z.array(idDocumentInputSchema$1).optional(),
570
- address: createAddressInputSchema,
618
+ address: ownerAddressInputSchema,
571
619
  dateOfEstablishment: z.string().min(1, "Datum vzniku funkce je povinn\xFD")
572
620
  });
573
621
  const createPartyInputSchema = z.object({
@@ -583,6 +631,21 @@ const createPartyInputSchema = z.object({
583
631
  recipients: z.array(bankAccountInputSchema),
584
632
  products: productsInputSchema$1.optional(),
585
633
  traderIds: z.array(z.uuid()).optional()
634
+ }).refine(
635
+ (b) => b.party.customerStatus !== "BLOCKED" || !!b.party.blockReason,
636
+ {
637
+ message: "blockReason is required when customerStatus is BLOCKED",
638
+ path: ["party", "blockReason"]
639
+ }
640
+ ).refine(
641
+ (b) => b.party.customerStatus === "BLOCKED" || !b.party.blockReason && !b.party.blockReasonNote,
642
+ {
643
+ message: "blockReason and blockReasonNote must be null when customerStatus is not BLOCKED",
644
+ path: ["party", "blockReason"]
645
+ }
646
+ ).refine((b) => b.party.blockReason !== "OTHER" || !!b.party.blockReasonNote, {
647
+ message: "blockReasonNote is required when blockReason is OTHER",
648
+ path: ["party", "blockReasonNote"]
586
649
  });
587
650
  const partyCreateOutputSchema = z.object({
588
651
  customerStatus: z.enum(CUSTOMER_STATUS),
@@ -627,101 +690,211 @@ const createPartyRoute = createRoute({
627
690
  ...errorResponse("Party", "Creation")
628
691
  }
629
692
  });
630
- new OpenAPIHono().openapi(
631
- createPartyRoute,
632
- async (context) => {
633
- const { owners, legalRepresentatives, ...partyInput } = context.req.valid("json");
634
- const disponentsWithDocs = (partyInput.disponents ?? []).filter(
635
- (d) => d.partyType === "INDIVIDUAL" && !!d.data.identityDocuments?.length
636
- );
637
- const disponentsForBatch = (partyInput.disponents ?? []).filter(
638
- (d) => !(d.partyType === "INDIVIDUAL" && !!d.data.identityDocuments?.length)
693
+ new OpenAPIHono().openapi(createPartyRoute, async (context) => {
694
+ const { owners, legalRepresentatives, ...partyInput } = context.req.valid("json");
695
+ const disponentsWithDocs = (partyInput.disponents ?? []).filter(
696
+ (d) => d.partyType === "INDIVIDUAL" && !!d.data.identityDocuments?.length
697
+ );
698
+ const disponentsForBatch = (partyInput.disponents ?? []).filter(
699
+ (d) => !(d.partyType === "INDIVIDUAL" && !!d.data.identityDocuments?.length)
700
+ );
701
+ const { data: partyData, error } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.createParty({
702
+ ...partyInput,
703
+ disponents: disponentsForBatch,
704
+ party: { ...partyInput.party, isRoot: true }
705
+ });
706
+ if (!partyData || error) {
707
+ logAuditEvent(context, {
708
+ eventType: "party.created",
709
+ targetType: "party",
710
+ outcome: "failure",
711
+ severity: "medium",
712
+ description: "Failed to create party",
713
+ details: { partyType: partyInput.party.partyType }
714
+ });
715
+ return context.json(
716
+ {
717
+ message: "Could not create a party"
718
+ },
719
+ 500
639
720
  );
640
- const { data: partyData, error } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.createParty({
641
- ...partyInput,
642
- disponents: disponentsForBatch,
643
- party: { ...partyInput.party, isRoot: true }
721
+ }
722
+ const mdm = context.env.MASTER_DATA_MANAGEMENT_SERVICE;
723
+ const docService = context.env.DOCUMENT_SERVICE;
724
+ if (partyInput.organizationData?.registerUri) {
725
+ await docService.acknowledgeDocument({
726
+ storageUrl: partyInput.organizationData.registerUri,
727
+ entityId: partyData.id
644
728
  });
645
- if (!partyData || error) {
646
- return context.json(
647
- {
648
- message: "Could not create a party"
649
- },
650
- 500
651
- );
652
- }
653
- const mdm = context.env.MASTER_DATA_MANAGEMENT_SERVICE;
654
- const docService = context.env.DOCUMENT_SERVICE;
655
- if (partyInput.organizationData?.registerUri) {
729
+ }
730
+ for (const ba of partyInput.bankAccounts ?? []) {
731
+ if (ba.statementUri) {
656
732
  await docService.acknowledgeDocument({
657
- storageUrl: partyInput.organizationData.registerUri,
733
+ storageUrl: ba.statementUri,
658
734
  entityId: partyData.id
659
735
  });
660
736
  }
661
- for (const ba of partyInput.bankAccounts ?? []) {
662
- if (ba.statementUri) {
663
- await docService.acknowledgeDocument({
664
- storageUrl: ba.statementUri,
665
- entityId: partyData.id
666
- });
667
- }
737
+ }
738
+ for (const [i, doc] of (partyInput.personalData?.identityDocuments ?? []).entries()) {
739
+ const idDocId = partyData.personalIdDocIds?.[i] ?? partyData.id;
740
+ if (doc.frontImageUri) {
741
+ await docService.acknowledgeDocument({
742
+ storageUrl: doc.frontImageUri,
743
+ entityId: idDocId
744
+ });
745
+ }
746
+ if (doc.backImageUri) {
747
+ await docService.acknowledgeDocument({
748
+ storageUrl: doc.backImageUri,
749
+ entityId: idDocId
750
+ });
751
+ }
752
+ }
753
+ for (const disponent of disponentsWithDocs) {
754
+ const { identityDocuments, ...individualData } = disponent.data;
755
+ const { data: disponentParty, error: disponentError } = await mdm.createParty({
756
+ party: {
757
+ partyType: "INDIVIDUAL",
758
+ language: partyInput.party.language,
759
+ customerStatus: "NEW"
760
+ },
761
+ personalData: individualData,
762
+ organizationData: null,
763
+ bankAccounts: null,
764
+ recipients: null,
765
+ disponents: null,
766
+ addresses: [],
767
+ aml: null
768
+ });
769
+ if (!disponentParty || disponentError) {
770
+ return context.json({ message: "Could not create disponent party" }, 500);
771
+ }
772
+ const { error: linkDisponentError } = await mdm.linkPartyToParty({
773
+ fromPartyId: disponentParty.id,
774
+ toPartyId: partyData.id,
775
+ relationshipType: "AUTHORIZED_SIGNATORY"
776
+ });
777
+ if (linkDisponentError) {
778
+ return context.json({ message: "Could not link disponent to party" }, 500);
668
779
  }
669
- for (const [i, doc] of (partyInput.personalData?.identityDocuments ?? []).entries()) {
670
- const idDocId = partyData.personalIdDocIds?.[i] ?? partyData.id;
780
+ for (const doc of identityDocuments ?? []) {
781
+ const { data: createdIdDoc, error: idDocError } = await mdm.createIdDocument({
782
+ partyId: disponentParty.id,
783
+ idDocument: {
784
+ partyId: disponentParty.id,
785
+ idDocType: doc.idDocType,
786
+ idDocNumber: doc.idDocNumber,
787
+ idDocHolderName: `${individualData.name} ${individualData.surname}`,
788
+ issueDate: doc.issueDate,
789
+ expirationDate: doc.expirationDate,
790
+ issuer: doc.issuer,
791
+ frontImageUri: doc.frontImageUri,
792
+ backImageUri: doc.backImageUri
793
+ }
794
+ });
795
+ if (!createdIdDoc || idDocError) {
796
+ return context.json(
797
+ { message: "Could not create disponent identity document" },
798
+ 500
799
+ );
800
+ }
671
801
  if (doc.frontImageUri) {
672
802
  await docService.acknowledgeDocument({
673
803
  storageUrl: doc.frontImageUri,
674
- entityId: idDocId
804
+ entityId: createdIdDoc.id
675
805
  });
676
806
  }
677
807
  if (doc.backImageUri) {
678
808
  await docService.acknowledgeDocument({
679
809
  storageUrl: doc.backImageUri,
680
- entityId: idDocId
810
+ entityId: createdIdDoc.id
681
811
  });
682
812
  }
683
813
  }
684
- for (const disponent of disponentsWithDocs) {
685
- const { identityDocuments, ...individualData } = disponent.data;
686
- const { data: disponentParty, error: disponentError } = await mdm.createParty({
814
+ }
815
+ if (owners?.length) {
816
+ for (const owner of owners) {
817
+ const { address, sharePercentage, shareEstablishedAt, ...individual } = owner;
818
+ const { data: ownerParty, error: ownerError } = await mdm.createParty({
687
819
  party: {
688
820
  partyType: "INDIVIDUAL",
689
821
  language: partyInput.party.language,
690
822
  customerStatus: "NEW"
691
823
  },
692
- personalData: individualData,
824
+ personalData: { ...individual, email: individual.email ?? "" },
693
825
  organizationData: null,
694
826
  bankAccounts: null,
695
827
  recipients: null,
696
828
  disponents: null,
697
- addresses: [],
829
+ addresses: [
830
+ {
831
+ ...address,
832
+ district: address.district ?? "",
833
+ region: address.region ?? ""
834
+ }
835
+ ],
698
836
  aml: null
699
837
  });
700
- if (!disponentParty || disponentError) {
838
+ if (ownerParty && !ownerError) {
839
+ await mdm.linkPartyToParty({
840
+ fromPartyId: ownerParty.id,
841
+ toPartyId: partyData.id,
842
+ relationshipType: "UBO",
843
+ sharePercentage,
844
+ fromDate: new Date(shareEstablishedAt)
845
+ });
846
+ }
847
+ }
848
+ }
849
+ if (legalRepresentatives?.length) {
850
+ for (const rep of legalRepresentatives) {
851
+ const { address, identityDocuments, dateOfEstablishment, ...individual } = rep;
852
+ const { data: repParty, error: repError } = await mdm.createParty({
853
+ party: {
854
+ partyType: "INDIVIDUAL",
855
+ language: partyInput.party.language,
856
+ customerStatus: "NEW"
857
+ },
858
+ personalData: individual,
859
+ organizationData: null,
860
+ bankAccounts: null,
861
+ recipients: null,
862
+ disponents: null,
863
+ addresses: [
864
+ {
865
+ ...address,
866
+ district: address.district ?? "",
867
+ region: address.region ?? ""
868
+ }
869
+ ],
870
+ aml: null
871
+ });
872
+ if (!repParty || repError) {
701
873
  return context.json(
702
- { message: "Could not create disponent party" },
874
+ { message: "Could not create legal representative party" },
703
875
  500
704
876
  );
705
877
  }
706
- const { error: linkDisponentError } = await mdm.linkPartyToParty({
707
- fromPartyId: disponentParty.id,
878
+ const { error: linkRepError } = await mdm.linkPartyToParty({
879
+ fromPartyId: repParty.id,
708
880
  toPartyId: partyData.id,
709
- relationshipType: "AUTHORIZED_SIGNATORY"
881
+ relationshipType: "BOARD_MEMBER",
882
+ fromDate: new Date(dateOfEstablishment)
710
883
  });
711
- if (linkDisponentError) {
884
+ if (linkRepError) {
712
885
  return context.json(
713
- { message: "Could not link disponent to party" },
886
+ { message: "Could not link legal representative to party" },
714
887
  500
715
888
  );
716
889
  }
717
890
  for (const doc of identityDocuments ?? []) {
718
891
  const { data: createdIdDoc, error: idDocError } = await mdm.createIdDocument({
719
- partyId: disponentParty.id,
892
+ partyId: repParty.id,
720
893
  idDocument: {
721
- partyId: disponentParty.id,
894
+ partyId: repParty.id,
722
895
  idDocType: doc.idDocType,
723
896
  idDocNumber: doc.idDocNumber,
724
- idDocHolderName: `${individualData.name} ${individualData.surname}`,
897
+ idDocHolderName: `${individual.name} ${individual.surname}`,
725
898
  issueDate: doc.issueDate,
726
899
  expirationDate: doc.expirationDate,
727
900
  issuer: doc.issuer,
@@ -731,7 +904,9 @@ new OpenAPIHono().openapi(
731
904
  });
732
905
  if (!createdIdDoc || idDocError) {
733
906
  return context.json(
734
- { message: "Could not create disponent identity document" },
907
+ {
908
+ message: "Could not create legal representative identity document"
909
+ },
735
910
  500
736
911
  );
737
912
  }
@@ -749,121 +924,30 @@ new OpenAPIHono().openapi(
749
924
  }
750
925
  }
751
926
  }
752
- if (owners?.length) {
753
- for (const owner of owners) {
754
- const { address, sharePercentage, shareEstablishedAt, ...individual } = owner;
755
- const { data: ownerParty, error: ownerError } = await mdm.createParty({
756
- party: {
757
- partyType: "INDIVIDUAL",
758
- language: partyInput.party.language,
759
- customerStatus: "NEW"
760
- },
761
- personalData: individual,
762
- organizationData: null,
763
- bankAccounts: null,
764
- recipients: null,
765
- disponents: null,
766
- addresses: [{ ...address, addressType: "PERMANENT_ADDRESS" }],
767
- aml: null
768
- });
769
- if (ownerParty && !ownerError) {
770
- await mdm.linkPartyToParty({
771
- fromPartyId: ownerParty.id,
772
- toPartyId: partyData.id,
773
- relationshipType: "UBO",
774
- sharePercentage,
775
- fromDate: new Date(shareEstablishedAt)
776
- });
777
- }
778
- }
779
- }
780
- if (legalRepresentatives?.length) {
781
- for (const rep of legalRepresentatives) {
782
- const {
783
- address,
784
- identityDocuments,
785
- dateOfEstablishment,
786
- ...individual
787
- } = rep;
788
- const { data: repParty, error: repError } = await mdm.createParty({
789
- party: {
790
- partyType: "INDIVIDUAL",
791
- language: partyInput.party.language,
792
- customerStatus: "NEW"
793
- },
794
- personalData: individual,
795
- organizationData: null,
796
- bankAccounts: null,
797
- recipients: null,
798
- disponents: null,
799
- addresses: [{ ...address, addressType: "PERMANENT_ADDRESS" }],
800
- aml: null
801
- });
802
- if (!repParty || repError) {
803
- return context.json(
804
- { message: "Could not create legal representative party" },
805
- 500
806
- );
807
- }
808
- const { error: linkRepError } = await mdm.linkPartyToParty({
809
- fromPartyId: repParty.id,
810
- toPartyId: partyData.id,
811
- relationshipType: "BOARD_MEMBER",
812
- fromDate: new Date(dateOfEstablishment)
813
- });
814
- if (linkRepError) {
815
- return context.json(
816
- { message: "Could not link legal representative to party" },
817
- 500
818
- );
819
- }
820
- for (const doc of identityDocuments ?? []) {
821
- const { data: createdIdDoc, error: idDocError } = await mdm.createIdDocument({
822
- partyId: repParty.id,
823
- idDocument: {
824
- partyId: repParty.id,
825
- idDocType: doc.idDocType,
826
- idDocNumber: doc.idDocNumber,
827
- idDocHolderName: `${individual.name} ${individual.surname}`,
828
- issueDate: doc.issueDate,
829
- expirationDate: doc.expirationDate,
830
- issuer: doc.issuer,
831
- frontImageUri: doc.frontImageUri,
832
- backImageUri: doc.backImageUri
833
- }
834
- });
835
- if (!createdIdDoc || idDocError) {
836
- return context.json(
837
- {
838
- message: "Could not create legal representative identity document"
839
- },
840
- 500
841
- );
842
- }
843
- if (doc.frontImageUri) {
844
- await docService.acknowledgeDocument({
845
- storageUrl: doc.frontImageUri,
846
- entityId: createdIdDoc.id
847
- });
848
- }
849
- if (doc.backImageUri) {
850
- await docService.acknowledgeDocument({
851
- storageUrl: doc.backImageUri,
852
- entityId: createdIdDoc.id
853
- });
854
- }
855
- }
856
- }
857
- }
858
- return context.json(
859
- {
860
- message: "party created",
861
- party: partyData
862
- },
863
- 200
864
- );
865
927
  }
866
- );
928
+ logAuditEvent(context, {
929
+ eventType: "party.created",
930
+ targetType: "party",
931
+ targetId: partyData.id,
932
+ outcome: "success",
933
+ severity: "medium",
934
+ description: `Party ${partyData.id} created`,
935
+ details: {
936
+ partyType: partyInput.party.partyType,
937
+ disponentCount: partyInput.disponents?.length ?? 0,
938
+ bankAccountCount: partyInput.bankAccounts?.length ?? 0,
939
+ ownerCount: owners?.length ?? 0,
940
+ legalRepresentativeCount: legalRepresentatives?.length ?? 0
941
+ }
942
+ });
943
+ return context.json(
944
+ {
945
+ message: "party created",
946
+ party: partyData
947
+ },
948
+ 200
949
+ );
950
+ });
867
951
 
868
952
  const partySchema = z.object({
869
953
  partyType: z.enum(PARTY_TYPE),
@@ -871,7 +955,10 @@ const partySchema = z.object({
871
955
  id: z.uuid(),
872
956
  internalId: z.string().optional(),
873
957
  note: z.string().optional(),
874
- countryCode: z.enum(COUNTRY_CODES).optional()
958
+ countryCode: z.enum(COUNTRY_CODES).optional(),
959
+ customerStatus: z.enum(CUSTOMER_STATUS).optional(),
960
+ blockReason: z.enum(BLOCK_REASON).nullable().optional(),
961
+ blockReasonNote: z.string().trim().min(1).nullable().optional()
875
962
  });
876
963
  const idDocumentInputSchema = z.object({
877
964
  id: z.uuid().optional(),
@@ -1060,7 +1147,7 @@ const updateAmlInputSchema = z.object({
1060
1147
  personName: z.string(),
1061
1148
  personRole: z.enum(PERSON_ROLE),
1062
1149
  answer: z.enum(YES_NO_UNKNOWN),
1063
- country: z.enum(COUNTRY_CODES).nullable()
1150
+ countries: z.array(z.enum(COUNTRY_CODES))
1064
1151
  })
1065
1152
  ).optional()
1066
1153
  });
@@ -1079,7 +1166,7 @@ const ownerSyncSchema = z.object({
1079
1166
  name: z.string(),
1080
1167
  surname: z.string(),
1081
1168
  titleAfter: z.string().optional(),
1082
- email: z.email("E-mail je povinn\xFD"),
1169
+ email: z.email("E-mail je povinn\xFD").optional(),
1083
1170
  birthDate: z.string(),
1084
1171
  birthPlace: z.string().optional(),
1085
1172
  personalId: z.string(),
@@ -1122,6 +1209,21 @@ const partyUpdateInputSchema = z.object({
1122
1209
  aml: updateAmlInputSchema.optional(),
1123
1210
  products: productsInputSchema.optional(),
1124
1211
  traderIds: z.array(z.uuid()).optional()
1212
+ }).refine(
1213
+ (b) => b.party.customerStatus !== "BLOCKED" || !!b.party.blockReason,
1214
+ {
1215
+ message: "blockReason is required when customerStatus is BLOCKED",
1216
+ path: ["party", "blockReason"]
1217
+ }
1218
+ ).refine(
1219
+ (b) => b.party.customerStatus === "BLOCKED" || b.party.customerStatus === void 0 || !b.party.blockReason && !b.party.blockReasonNote,
1220
+ {
1221
+ message: "blockReason and blockReasonNote must be null when customerStatus is not BLOCKED",
1222
+ path: ["party", "blockReason"]
1223
+ }
1224
+ ).refine((b) => b.party.blockReason !== "OTHER" || !!b.party.blockReasonNote, {
1225
+ message: "blockReasonNote is required when blockReason is OTHER",
1226
+ path: ["party", "blockReasonNote"]
1125
1227
  });
1126
1228
  const updatePartyOuputSchema = z.object({
1127
1229
  id: z.uuid(),
@@ -1169,48 +1271,75 @@ const updatePartyRoute = createRoute({
1169
1271
  ...errorResponse("Party", "Update")
1170
1272
  }
1171
1273
  });
1172
- new OpenAPIHono().openapi(
1173
- updatePartyRoute,
1174
- async (context) => {
1175
- const party = context.req.valid("json");
1176
- const { partyId } = context.req.valid("param");
1177
- const { data: updatedParty, error: partyError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.updateParty({
1178
- partyId,
1179
- party: party.party,
1180
- personalData: party.personalData ?? void 0,
1181
- organizationData: party.organizationData ?? void 0,
1182
- bankAccounts: party.bankAccounts ?? void 0,
1183
- disponents: party.disponents ?? void 0,
1184
- addresses: party.addresses ?? void 0,
1185
- recipients: party.recipients ?? void 0,
1186
- aml: party.aml ?? void 0,
1187
- products: party.products ?? void 0,
1188
- traderIds: party.traderIds ?? void 0,
1189
- legalRepresentatives: party.legalRepresentatives ?? void 0,
1190
- owners: party.owners ?? void 0
1274
+ new OpenAPIHono().openapi(updatePartyRoute, async (context) => {
1275
+ const party = context.req.valid("json");
1276
+ const { partyId } = context.req.valid("param");
1277
+ const { data: updatedParty, error: partyError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.updateParty({
1278
+ partyId,
1279
+ party: party.party,
1280
+ personalData: party.personalData ?? void 0,
1281
+ organizationData: party.organizationData ?? void 0,
1282
+ bankAccounts: party.bankAccounts ?? void 0,
1283
+ disponents: party.disponents ?? void 0,
1284
+ addresses: party.addresses ?? void 0,
1285
+ recipients: party.recipients ?? void 0,
1286
+ aml: party.aml ?? void 0,
1287
+ products: party.products ?? void 0,
1288
+ traderIds: party.traderIds ?? void 0,
1289
+ legalRepresentatives: party.legalRepresentatives ?? void 0,
1290
+ owners: party.owners ?? void 0
1291
+ });
1292
+ if (!updatedParty || partyError) {
1293
+ logAuditEvent(context, {
1294
+ eventType: "party.updated",
1295
+ targetType: "party",
1296
+ targetId: partyId,
1297
+ outcome: "failure",
1298
+ severity: "medium",
1299
+ description: `Failed to update party ${partyId}`,
1300
+ details: { partyType: party.party.partyType }
1191
1301
  });
1192
- if (!updatedParty || partyError) {
1193
- return context.json({ message: "Could not update a Party" }, 404);
1194
- }
1195
- const docService = context.env.DOCUMENT_SERVICE;
1196
- if (party.organizationData?.registerUri) {
1302
+ return context.json({ message: "Could not update a Party" }, 404);
1303
+ }
1304
+ const docService = context.env.DOCUMENT_SERVICE;
1305
+ if (party.organizationData?.registerUri) {
1306
+ await docService.acknowledgeDocument({
1307
+ storageUrl: party.organizationData.registerUri,
1308
+ entityId: partyId
1309
+ });
1310
+ }
1311
+ for (const ba of party.bankAccounts ?? []) {
1312
+ if (ba.statementUri) {
1197
1313
  await docService.acknowledgeDocument({
1198
- storageUrl: party.organizationData.registerUri,
1314
+ storageUrl: ba.statementUri,
1199
1315
  entityId: partyId
1200
1316
  });
1201
1317
  }
1202
- for (const ba of party.bankAccounts ?? []) {
1203
- if (ba.statementUri) {
1318
+ }
1319
+ if (party.personalData?.identityDocuments?.length) {
1320
+ let newDocIdx = 0;
1321
+ for (const doc of party.personalData.identityDocuments) {
1322
+ const idDocEntityId = doc.id ? doc.id : updatedParty.newPersonalIdDocIds?.[newDocIdx++] ?? partyId;
1323
+ if (doc.frontImageUri) {
1204
1324
  await docService.acknowledgeDocument({
1205
- storageUrl: ba.statementUri,
1206
- entityId: partyId
1325
+ storageUrl: doc.frontImageUri,
1326
+ entityId: idDocEntityId
1327
+ });
1328
+ }
1329
+ if (doc.backImageUri) {
1330
+ await docService.acknowledgeDocument({
1331
+ storageUrl: doc.backImageUri,
1332
+ entityId: idDocEntityId
1207
1333
  });
1208
1334
  }
1209
1335
  }
1210
- if (party.personalData?.identityDocuments?.length) {
1211
- let newDocIdx = 0;
1212
- for (const doc of party.personalData.identityDocuments) {
1213
- const idDocEntityId = doc.id ? doc.id : updatedParty.newPersonalIdDocIds?.[newDocIdx++] ?? partyId;
1336
+ }
1337
+ if (party.legalRepresentatives?.length) {
1338
+ let newBmDocIdx = 0;
1339
+ for (const rep of party.legalRepresentatives) {
1340
+ if (rep.id) continue;
1341
+ for (const doc of rep.identityDocuments ?? []) {
1342
+ const idDocEntityId = updatedParty.newBoardMemberIdDocIds?.[newBmDocIdx++] ?? partyId;
1214
1343
  if (doc.frontImageUri) {
1215
1344
  await docService.acknowledgeDocument({
1216
1345
  storageUrl: doc.frontImageUri,
@@ -1225,54 +1354,45 @@ new OpenAPIHono().openapi(
1225
1354
  }
1226
1355
  }
1227
1356
  }
1228
- if (party.legalRepresentatives?.length) {
1229
- let newBmDocIdx = 0;
1230
- for (const rep of party.legalRepresentatives) {
1231
- if (rep.id) continue;
1232
- for (const doc of rep.identityDocuments ?? []) {
1233
- const idDocEntityId = updatedParty.newBoardMemberIdDocIds?.[newBmDocIdx++] ?? partyId;
1234
- if (doc.frontImageUri) {
1235
- await docService.acknowledgeDocument({
1236
- storageUrl: doc.frontImageUri,
1237
- entityId: idDocEntityId
1238
- });
1239
- }
1240
- if (doc.backImageUri) {
1241
- await docService.acknowledgeDocument({
1242
- storageUrl: doc.backImageUri,
1243
- entityId: idDocEntityId
1244
- });
1245
- }
1357
+ for (const rep of party.legalRepresentatives) {
1358
+ if (!rep.id) continue;
1359
+ for (const doc of rep.identityDocuments ?? []) {
1360
+ const idDocEntityId = doc.id ? doc.id : updatedParty.newBoardMemberIdDocIds?.[newBmDocIdx++] ?? partyId;
1361
+ if (doc.frontImageUri) {
1362
+ await docService.acknowledgeDocument({
1363
+ storageUrl: doc.frontImageUri,
1364
+ entityId: idDocEntityId
1365
+ });
1246
1366
  }
1247
- }
1248
- for (const rep of party.legalRepresentatives) {
1249
- if (!rep.id) continue;
1250
- for (const doc of rep.identityDocuments ?? []) {
1251
- const idDocEntityId = doc.id ? doc.id : updatedParty.newBoardMemberIdDocIds?.[newBmDocIdx++] ?? partyId;
1252
- if (doc.frontImageUri) {
1253
- await docService.acknowledgeDocument({
1254
- storageUrl: doc.frontImageUri,
1255
- entityId: idDocEntityId
1256
- });
1257
- }
1258
- if (doc.backImageUri) {
1259
- await docService.acknowledgeDocument({
1260
- storageUrl: doc.backImageUri,
1261
- entityId: idDocEntityId
1262
- });
1263
- }
1367
+ if (doc.backImageUri) {
1368
+ await docService.acknowledgeDocument({
1369
+ storageUrl: doc.backImageUri,
1370
+ entityId: idDocEntityId
1371
+ });
1264
1372
  }
1265
1373
  }
1266
1374
  }
1267
- return context.json(
1268
- {
1269
- message: "Party updated successfully",
1270
- party: updatedParty
1271
- },
1272
- 200
1273
- );
1274
1375
  }
1275
- );
1376
+ logAuditEvent(context, {
1377
+ eventType: "party.updated",
1378
+ targetType: "party",
1379
+ targetId: partyId,
1380
+ outcome: "success",
1381
+ severity: "medium",
1382
+ description: `Party ${partyId} updated`,
1383
+ details: {
1384
+ partyType: party.party.partyType,
1385
+ customerStatus: updatedParty.customerStatus
1386
+ }
1387
+ });
1388
+ return context.json(
1389
+ {
1390
+ message: "Party updated successfully",
1391
+ party: updatedParty
1392
+ },
1393
+ 200
1394
+ );
1395
+ });
1276
1396
 
1277
1397
  const DOCUMENT_SIDE = ["front", "back"];
1278
1398
  const paramsSchema$2 = z.object({
@@ -1376,64 +1496,91 @@ const createDocumentRoute = createRoute({
1376
1496
  }
1377
1497
  }
1378
1498
  });
1379
- new OpenAPIHono().openapi(
1380
- createDocumentRoute,
1381
- async (context) => {
1382
- const rawBody = await context.req.parseBody();
1383
- const { partyId } = context.req.valid("param");
1384
- const idDocumentRaw = rawBody.idDocument;
1385
- const documentSide = rawBody.documentSide;
1386
- const file = rawBody.file;
1387
- if (typeof idDocumentRaw !== "string" || !DOCUMENT_SIDE.includes(documentSide) || !(file instanceof File)) {
1388
- return context.json({ message: "Invalid multipart payload" }, 400);
1389
- }
1390
- const idDocumentJson = JSON.parse(JSON.parse(idDocumentRaw));
1391
- const idDocumentResult = idDocumentCreateInputSchema.safeParse(idDocumentJson);
1392
- if (!idDocumentResult.success) {
1393
- console.error(idDocumentResult.error);
1394
- return context.json({ message: "Could not parse IdDocument" }, 400);
1395
- }
1396
- const {
1397
- data: createdIdDocument,
1398
- error: createError,
1399
- message
1400
- } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.createIdDocument({
1401
- idDocument: idDocumentResult.data,
1402
- partyId
1499
+ new OpenAPIHono().openapi(createDocumentRoute, async (context) => {
1500
+ const rawBody = await context.req.parseBody();
1501
+ const { partyId } = context.req.valid("param");
1502
+ const idDocumentRaw = rawBody.idDocument;
1503
+ const documentSide = rawBody.documentSide;
1504
+ const file = rawBody.file;
1505
+ if (typeof idDocumentRaw !== "string" || !DOCUMENT_SIDE.includes(documentSide) || !(file instanceof File)) {
1506
+ return context.json({ message: "Invalid multipart payload" }, 400);
1507
+ }
1508
+ const idDocumentJson = JSON.parse(JSON.parse(idDocumentRaw));
1509
+ const idDocumentResult = idDocumentCreateInputSchema.safeParse(idDocumentJson);
1510
+ if (!idDocumentResult.success) {
1511
+ console.error(idDocumentResult.error);
1512
+ return context.json({ message: "Could not parse IdDocument" }, 400);
1513
+ }
1514
+ const {
1515
+ data: createdIdDocument,
1516
+ error: createError,
1517
+ message
1518
+ } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.createIdDocument({
1519
+ idDocument: idDocumentResult.data,
1520
+ partyId
1521
+ });
1522
+ if (!createdIdDocument || createError) {
1523
+ console.error(message);
1524
+ logAuditEvent(context, {
1525
+ eventType: "id-document.created",
1526
+ targetType: "id-document",
1527
+ outcome: "failure",
1528
+ severity: "medium",
1529
+ description: `Failed to create ID document for party ${partyId}`,
1530
+ details: { partyId, idDocType: idDocumentResult.data.idDocType }
1403
1531
  });
1404
- if (!createdIdDocument || createError) {
1405
- console.error(message);
1406
- return context.json({ message: "Could not create IdDocument" }, 500);
1532
+ return context.json({ message: "Could not create IdDocument" }, 500);
1533
+ }
1534
+ const bytes = new Uint8Array(await file.arrayBuffer());
1535
+ const { data: uploadResult, error: uploadError } = await context.env.DOCUMENT_SERVICE.uploadDocument({
1536
+ entityType: "client",
1537
+ entityId: createdIdDocument.id,
1538
+ type: "kyc",
1539
+ metadata: {},
1540
+ file: {
1541
+ bytes,
1542
+ type: file.type,
1543
+ name: file.name,
1544
+ size: file.size
1407
1545
  }
1408
- const bytes = new Uint8Array(await file.arrayBuffer());
1409
- const { data: uploadResult, error: uploadError } = await context.env.DOCUMENT_SERVICE.uploadDocument({
1410
- entityType: "client",
1411
- entityId: createdIdDocument.id,
1412
- type: "kyc",
1413
- metadata: {},
1414
- file: {
1415
- bytes,
1416
- type: file.type,
1417
- name: file.name,
1418
- size: file.size
1419
- }
1546
+ });
1547
+ if (!uploadResult || uploadError) {
1548
+ logAuditEvent(context, {
1549
+ eventType: "id-document.created",
1550
+ targetType: "id-document",
1551
+ targetId: createdIdDocument.id,
1552
+ outcome: "error",
1553
+ severity: "medium",
1554
+ description: `ID document ${createdIdDocument.id} created but file upload failed`,
1555
+ details: { partyId, documentSide }
1420
1556
  });
1421
- if (!uploadResult || uploadError) {
1422
- return context.json({ message: "File upload failed" }, 500);
1423
- }
1424
- return context.json(
1425
- {
1426
- message: "IdDocument created successfully",
1427
- idDocument: {
1428
- ...createdIdDocument,
1429
- frontImageUri: documentSide === "front" ? uploadResult.storageUrl : createdIdDocument.frontImageUri,
1430
- backImageUri: documentSide === "back" ? uploadResult.storageUrl : createdIdDocument.backImageUri
1431
- }
1432
- },
1433
- 200
1434
- );
1557
+ return context.json({ message: "File upload failed" }, 500);
1435
1558
  }
1436
- );
1559
+ logAuditEvent(context, {
1560
+ eventType: "id-document.created",
1561
+ targetType: "id-document",
1562
+ targetId: createdIdDocument.id,
1563
+ outcome: "success",
1564
+ severity: "medium",
1565
+ description: `ID document ${createdIdDocument.id} created for party ${partyId}`,
1566
+ details: {
1567
+ partyId,
1568
+ idDocType: createdIdDocument.idDocType,
1569
+ documentSide
1570
+ }
1571
+ });
1572
+ return context.json(
1573
+ {
1574
+ message: "IdDocument created successfully",
1575
+ idDocument: {
1576
+ ...createdIdDocument,
1577
+ frontImageUri: documentSide === "front" ? uploadResult.storageUrl : createdIdDocument.frontImageUri,
1578
+ backImageUri: documentSide === "back" ? uploadResult.storageUrl : createdIdDocument.backImageUri
1579
+ }
1580
+ },
1581
+ 200
1582
+ );
1583
+ });
1437
1584
 
1438
1585
  const partyBaseOutputSchema = z.object({
1439
1586
  customerStatus: z.enum(CUSTOMER_STATUS),
@@ -1521,7 +1668,7 @@ const bankAccountOutputSchema = z.object({
1521
1668
  currency: z.enum(CURRENCY_CODES),
1522
1669
  countryCode: z.enum(COUNTRY_CODES),
1523
1670
  iban: z.string().nullable(),
1524
- address: z.string().nullable(),
1671
+ address: structuredAddressSchema.nullable(),
1525
1672
  swiftBic: z.string().nullable(),
1526
1673
  bicCor: z.string().nullable(),
1527
1674
  routingNumber: z.string().nullable(),
@@ -1612,7 +1759,7 @@ const createAmlOutputSchema = z.object({
1612
1759
  personName: z.string(),
1613
1760
  personRole: z.enum(PERSON_ROLE),
1614
1761
  answer: z.enum(YES_NO_UNKNOWN),
1615
- country: z.enum(COUNTRY_CODES).nullable()
1762
+ countries: z.array(z.enum(COUNTRY_CODES))
1616
1763
  })
1617
1764
  ).nullable(),
1618
1765
  createdAt: z.iso.datetime().nullable(),
@@ -1730,7 +1877,12 @@ const paginationAndSearchSchema = z.object({
1730
1877
  column: z.string(),
1731
1878
  direction: z.enum(["asc", "desc"]),
1732
1879
  search: z.string().optional(),
1733
- ids: z.string().array().optional(),
1880
+ // Hono validator collapses single-value query keys to strings, so accept
1881
+ // both `?ids=<uuid>` and `?ids=<uuid>&ids=<uuid>` and normalize to array.
1882
+ ids: z.preprocess(
1883
+ (v) => v == null ? void 0 : Array.isArray(v) ? v : [v],
1884
+ z.string().array().optional()
1885
+ ),
1734
1886
  includeDeleted: z.string().transform((v) => v === "true").optional()
1735
1887
  });
1736
1888
  const partiesOutputDataSchema = z.array(partyOutputSchema);
@@ -1948,35 +2100,57 @@ const updateIdDocumentRoute = createRoute({
1948
2100
  }
1949
2101
  }
1950
2102
  });
1951
- new OpenAPIHono().openapi(
1952
- updateIdDocumentRoute,
1953
- async (context) => {
1954
- const { id } = context.req.valid("param");
1955
- const patch = context.req.valid("json");
1956
- const { data: existingDocument, error: fetchError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.getIdDocument({ id });
1957
- if (!existingDocument || fetchError) {
1958
- return context.json({ message: "ID document not found" }, 404);
1959
- }
1960
- const fullIdDocument = {
1961
- ...existingDocument,
1962
- ...patch,
1963
- id
1964
- };
1965
- const { data: updatedIdDocument, error: updateError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.updateIdDocument({
1966
- idDocument: fullIdDocument
2103
+ new OpenAPIHono().openapi(updateIdDocumentRoute, async (context) => {
2104
+ const { id } = context.req.valid("param");
2105
+ const patch = context.req.valid("json");
2106
+ const { data: existingDocument, error: fetchError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.getIdDocument({ id });
2107
+ if (!existingDocument || fetchError) {
2108
+ logAuditEvent(context, {
2109
+ eventType: "id-document.updated",
2110
+ targetType: "id-document",
2111
+ targetId: id,
2112
+ outcome: "failure",
2113
+ severity: "medium",
2114
+ description: `Failed to update ID document ${id}: not found`
1967
2115
  });
1968
- if (!updatedIdDocument || updateError) {
1969
- return context.json({ message: "Could not update ID document" }, 500);
1970
- }
1971
- return context.json(
1972
- {
1973
- message: "ID document updated successfully",
1974
- idDocument: updatedIdDocument
1975
- },
1976
- 200
1977
- );
2116
+ return context.json({ message: "ID document not found" }, 404);
1978
2117
  }
1979
- );
2118
+ const fullIdDocument = {
2119
+ ...existingDocument,
2120
+ ...patch,
2121
+ id
2122
+ };
2123
+ const { data: updatedIdDocument, error: updateError } = await context.env.MASTER_DATA_MANAGEMENT_SERVICE.updateIdDocument({
2124
+ idDocument: fullIdDocument
2125
+ });
2126
+ if (!updatedIdDocument || updateError) {
2127
+ logAuditEvent(context, {
2128
+ eventType: "id-document.updated",
2129
+ targetType: "id-document",
2130
+ targetId: id,
2131
+ outcome: "failure",
2132
+ severity: "medium",
2133
+ description: `Failed to update ID document ${id}`
2134
+ });
2135
+ return context.json({ message: "Could not update ID document" }, 500);
2136
+ }
2137
+ logAuditEvent(context, {
2138
+ eventType: "id-document.updated",
2139
+ targetType: "id-document",
2140
+ targetId: id,
2141
+ outcome: "success",
2142
+ severity: "medium",
2143
+ description: `ID document ${id} updated`,
2144
+ details: { partyId: updatedIdDocument.partyId }
2145
+ });
2146
+ return context.json(
2147
+ {
2148
+ message: "ID document updated successfully",
2149
+ idDocument: updatedIdDocument
2150
+ },
2151
+ 200
2152
+ );
2153
+ });
1980
2154
 
1981
2155
  const messageResponseSchema = z.object({
1982
2156
  message: z.string()