diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 173bd797..a2ddf018 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -84,6 +84,7 @@ import ( infrastructurev1alpha1 "go.miloapis.com/milo/pkg/apis/infrastructure/v1alpha1" notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" + vendorsv1alpha1 "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log" @@ -128,6 +129,7 @@ func init() { utilruntime.Must(iamv1alpha1.AddToScheme(Scheme)) utilruntime.Must(notificationv1alpha1.AddToScheme(Scheme)) utilruntime.Must(apiregistrationv1.AddToScheme(Scheme)) + utilruntime.Must(vendorsv1alpha1.AddToScheme(Scheme)) } const ( diff --git a/config/controller-manager/overlays/core-control-plane/rbac/role.yaml b/config/controller-manager/overlays/core-control-plane/rbac/role.yaml index acfa9d3c..55302d79 100644 --- a/config/controller-manager/overlays/core-control-plane/rbac/role.yaml +++ b/config/controller-manager/overlays/core-control-plane/rbac/role.yaml @@ -8,6 +8,7 @@ rules: - "" resources: - namespaces + - secrets verbs: - create - delete @@ -178,3 +179,31 @@ rules: - projects/finalizers verbs: - update +- apiGroups: + - vendors.miloapis.com + resources: + - vendors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - vendors.miloapis.com + resources: + - vendors/status + verbs: + - get + - patch + - update +- apiGroups: + - vendors.miloapis.com + resources: + - vendortypedefinitions + verbs: + - get + - list + - watch diff --git a/config/crd/bases/vendors/kustomization.yaml b/config/crd/bases/vendors/kustomization.yaml new file mode 100644 index 00000000..0851c601 --- /dev/null +++ b/config/crd/bases/vendors/kustomization.yaml @@ -0,0 +1,4 @@ +resources: +- vendors.miloapis.com_vendors.yaml +- vendors.miloapis.com_vendortypedefinitions.yaml +- vendors.miloapis.com_vendorverifications.yaml diff --git a/config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml new file mode 100644 index 00000000..30da2e9a --- /dev/null +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml @@ -0,0 +1,330 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vendors.vendors.miloapis.com +spec: + group: vendors.miloapis.com + names: + categories: + - datum + kind: Vendor + listKind: VendorList + plural: vendors + singular: vendor + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.legalName + name: Legal Name + type: string + - jsonPath: .spec.profileType + name: Profile Type + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .status.verificationStatus + name: Verification + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Vendor is the Schema for the Vendors API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VendorSpec defines the desired state of Vendor + properties: + billingAddress: + description: Billing address + properties: + city: + description: City + type: string + country: + description: Country + type: string + postalCode: + description: Postal or ZIP code + type: string + state: + description: State or province + type: string + street: + description: Street address line 1 + type: string + street2: + description: Street address line 2 (optional) + type: string + required: + - city + - country + - postalCode + - state + - street + type: object + corporationDBA: + description: Doing business as name + type: string + description: + description: Description of the vendor + type: string + legalName: + description: Legal name of the vendor (required) + type: string + mailingAddress: + description: Mailing address (if different from billing) + properties: + city: + description: City + type: string + country: + description: Country + type: string + postalCode: + description: Postal or ZIP code + type: string + state: + description: State or province + type: string + street: + description: Street address line 1 + type: string + street2: + description: Street address line 2 (optional) + type: string + required: + - city + - country + - postalCode + - state + - street + type: object + nickname: + description: Nickname or display name + type: string + profileType: + description: Profile type - person or business + enum: + - person + - business + type: string + registrationNumber: + description: Registration number (optional) + type: string + stateOfIncorporation: + description: State of incorporation + type: string + taxInfo: + description: Tax information + properties: + country: + description: Country for tax purposes + type: string + taxDocument: + description: Tax document reference (e.g., W-9, W-8BEN) + type: string + taxIdRef: + description: Reference to the tax identification number stored + in a Secret + properties: + namespace: + description: Namespace of the Secret (if empty, uses the same + namespace as the Vendor) + type: string + secretKey: + description: Key within the Secret that contains the tax ID + type: string + secretName: + description: Name of the Secret containing the tax ID + type: string + required: + - secretKey + - secretName + type: object + taxIdType: + description: Type of tax identification + enum: + - SSN + - EIN + - ITIN + - UNSPECIFIED + type: string + required: + - country + - taxDocument + - taxIdRef + - taxIdType + type: object + vendorType: + description: Business-specific fields (only applicable when profileType + is business) + pattern: ^[a-z0-9-]+$ + type: string + website: + description: Website URL + type: string + required: + - billingAddress + - legalName + - profileType + - taxInfo + type: object + status: + description: VendorStatus defines the observed state of Vendor + properties: + activatedAt: + description: Timestamp when vendor was activated + format: date-time + type: string + completedVerifications: + description: Number of completed verifications + format: int32 + type: integer + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for control plane to reconcile + reason: Unknown + status: Unknown + type: Ready + description: |- + Conditions represents the observations of a vendor's current state. + Known condition types are: "Ready", "Validated", "Verified", "Active" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + expiredVerifications: + description: Number of expired verifications + format: int32 + type: integer + lastVerifiedAt: + description: Timestamp when vendor was last verified + format: date-time + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this Vendor by the controller. + format: int64 + type: integer + pendingVerifications: + description: Number of pending verifications + format: int32 + type: integer + rejectedAt: + description: Timestamp when vendor was rejected + format: date-time + type: string + rejectedVerifications: + description: Number of rejected verifications + format: int32 + type: integer + rejectionReason: + description: Reason for rejection (if applicable) + type: string + requiredVerifications: + description: Number of required verifications + format: int32 + type: integer + status: + description: Current status of the vendor + enum: + - pending + - active + - rejected + - archived + type: string + verificationStatus: + description: Overall verification status + enum: + - pending + - in-progress + - approved + - rejected + - expired + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml new file mode 100644 index 00000000..6c02cc12 --- /dev/null +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml @@ -0,0 +1,202 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vendortypedefinitions.vendors.miloapis.com +spec: + group: vendors.miloapis.com + names: + categories: + - datum + kind: VendorTypeDefinition + listKind: VendorTypeDefinitionList + plural: vendortypedefinitions + singular: vendortypedefinition + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.code + name: Code + type: string + - jsonPath: .spec.displayName + name: Display Name + type: string + - jsonPath: .spec.enabled + name: Enabled + type: boolean + - jsonPath: .spec.category + name: Category + type: string + - jsonPath: .status.vendorCount + name: Vendor Count + type: integer + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: VendorTypeDefinition is the Schema for the VendorTypeDefinitions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VendorTypeDefinitionSpec defines the desired state of VendorTypeDefinition + properties: + category: + description: Category of vendor type (e.g., "business", "nonprofit", + "international") + type: string + code: + description: The unique identifier for this vendor type (e.g., "llc", + "s-corp", "partnership") + pattern: ^[a-z0-9-]+$ + type: string + description: + description: Optional description of this vendor type + type: string + displayName: + description: Human-readable display name + type: string + enabled: + default: true + description: Whether this vendor type is currently available for selection + type: boolean + requiredTaxDocuments: + description: Required tax document types for this vendor type + items: + type: string + type: array + requiresBusinessFields: + default: false + description: Whether this type requires additional business-specific + fields + type: boolean + requiresTaxVerification: + default: true + description: Whether this type requires tax verification + type: boolean + validCountries: + description: Countries where this vendor type is valid (empty means + all countries) + items: + type: string + type: array + required: + - code + - displayName + - enabled + - requiresBusinessFields + - requiresTaxVerification + type: object + status: + description: VendorTypeDefinitionStatus defines the observed state of + VendorTypeDefinition + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for control plane to reconcile + reason: Unknown + status: Unknown + type: Ready + description: |- + Conditions represents the observations of a vendor type definition's current state. + Known condition types are: "Ready", "Valid" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastUsed: + description: Last time this type was used in a vendor + format: date-time + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this VendorTypeDefinition by the controller. + format: int64 + type: integer + vendorCount: + description: Number of vendors currently using this type + format: int32 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml new file mode 100644 index 00000000..f0394ed3 --- /dev/null +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml @@ -0,0 +1,277 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vendorverifications.vendors.miloapis.com +spec: + group: vendors.miloapis.com + names: + categories: + - datum + kind: VendorVerification + listKind: VendorVerificationList + plural: vendorverifications + singular: vendorverification + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.vendorRef.name + name: Vendor + type: string + - jsonPath: .spec.verificationType + name: Type + type: string + - jsonPath: .spec.status + name: Status + type: string + - jsonPath: .spec.verifierRef.name + name: Verifier + type: string + - jsonPath: .spec.required + name: Required + type: boolean + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: VendorVerification is the Schema for the VendorVerifications + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VendorVerificationSpec defines the desired state of VendorVerification + properties: + description: + description: Description of what is being verified + type: string + documents: + description: Documents used in this verification + items: + description: VerificationDocument represents a document used in + verification + properties: + expirationDate: + description: Document expiration date + format: date-time + type: string + reference: + description: Reference to the document (secret name, file path, + etc.) + type: string + type: + description: Type of document (W-9, W-8BEN, business-license, + etc.) + type: string + valid: + default: true + description: Whether the document is valid + type: boolean + version: + description: Document version or identifier + type: string + required: + - reference + - type + - valid + type: object + type: array + expirationDate: + description: Expiration date for this verification + format: date-time + type: string + externalReference: + description: External system reference (if verification is done by + external service) + type: string + notes: + description: Additional notes or comments about the verification + type: string + priority: + default: 5 + description: Priority of this verification (1-10, higher is more urgent) + format: int32 + maximum: 10 + minimum: 1 + type: integer + required: + default: true + description: Whether this verification is required for vendor activation + type: boolean + status: + default: pending + description: Current status of the verification + enum: + - pending + - in-progress + - approved + - rejected + - expired + type: string + vendorRef: + description: Reference to the vendor being verified + properties: + name: + description: Name of the Vendor resource + type: string + namespace: + description: Namespace of the Vendor resource (if empty, uses + the same namespace as the VendorVerification) + type: string + required: + - name + type: object + verificationType: + description: Type of verification being performed + enum: + - tax + - business + - identity + - compliance + - other + type: string + verifierRef: + description: Reference to who is performing the verification + properties: + metadata: + additionalProperties: + type: string + description: Additional metadata about the verifier + type: object + name: + description: Name of the verifier (username, service name, etc.) + type: string + type: + description: Type of verifier (user, system, external-service, + etc.) + enum: + - user + - system + - external-service + - admin + type: string + required: + - name + - type + type: object + required: + - priority + - required + - status + - vendorRef + - verificationType + type: object + status: + description: VendorVerificationStatus defines the observed state of VendorVerification + properties: + completedAt: + description: Timestamp when verification was completed + format: date-time + type: string + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for control plane to reconcile + reason: Unknown + status: Unknown + type: Ready + description: |- + Conditions represents the observations of a vendor verification's current state. + Known condition types are: "Ready", "Valid", "Expired" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastError: + description: Last error message if verification failed + type: string + lastUpdatedAt: + description: Timestamp when verification was last updated + format: date-time + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this VendorVerification by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/overlays/core-control-plane/kustomization.yaml b/config/crd/overlays/core-control-plane/kustomization.yaml index 8ae8e5bf..d88ac8ae 100644 --- a/config/crd/overlays/core-control-plane/kustomization.yaml +++ b/config/crd/overlays/core-control-plane/kustomization.yaml @@ -2,3 +2,4 @@ resources: - ../../bases/iam/ - ../../bases/resourcemanager/ - ../../bases/notification/ +- ../../bases/vendors/ diff --git a/config/samples/vendors/v1alpha1/tax-id-secrets-example.yaml b/config/samples/vendors/v1alpha1/tax-id-secrets-example.yaml new file mode 100644 index 00000000..b7e7ebfd --- /dev/null +++ b/config/samples/vendors/v1alpha1/tax-id-secrets-example.yaml @@ -0,0 +1,59 @@ +# Example Secret containing tax ID for ACME Corporation +apiVersion: v1 +kind: Secret +metadata: + name: acme-corp-tax-id + namespace: default + labels: + vendor.miloapis.com/vendor: acme-corp + vendor.miloapis.com/type: tax-id +type: Opaque +data: + # Base64 encoded tax ID: "12-3456789" + tax-id: MTItMzQ1Njc4OQ== +--- +# Example Secret containing tax ID for John Doe Consulting +apiVersion: v1 +kind: Secret +metadata: + name: john-doe-tax-id + namespace: default + labels: + vendor.miloapis.com/vendor: john-doe-consulting + vendor.miloapis.com/type: tax-id +type: Opaque +data: + # Base64 encoded tax ID: "123-45-6789" + ssn: MTIzLTQ1LTY3ODk= +--- +# Example Secret containing tax ID for international vendor +apiVersion: v1 +kind: Secret +metadata: + name: international-corp-tax-id + namespace: default + labels: + vendor.miloapis.com/vendor: international-corp + vendor.miloapis.com/type: tax-id +type: Opaque +data: + # Base64 encoded tax ID: "GB123456789" + vat-number: R0IxMjM0NTY3ODk= +--- +# Example Secret with multiple tax IDs for a complex vendor +apiVersion: v1 +kind: Secret +metadata: + name: complex-vendor-tax-ids + namespace: default + labels: + vendor.miloapis.com/vendor: complex-vendor + vendor.miloapis.com/type: tax-id +type: Opaque +data: + # EIN for US operations + ein: MTItMzQ1Njc4OQ== + # VAT number for EU operations + vat: R0IxMjM0NTY3ODk= + # Business number for Canadian operations + business-number: Q0ExMjM0NTY3ODk= diff --git a/config/samples/vendors/v1alpha1/vendor-example.yaml b/config/samples/vendors/v1alpha1/vendor-example.yaml new file mode 100644 index 00000000..5335b3b7 --- /dev/null +++ b/config/samples/vendors/v1alpha1/vendor-example.yaml @@ -0,0 +1,124 @@ +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp + annotations: + kubernetes.io/display-name: "ACME Corporation" +spec: + profileType: business + legalName: "ACME Corporation LLC" + nickname: "ACME Corp" + description: "A leading provider of innovative solutions" + website: "https://acme-corp.example.com" + vendorType: llc + corporationDBA: "ACME" + registrationNumber: "123456789" + stateOfIncorporation: "Delaware" + billingAddress: + street: "123 Business Ave" + street2: "Suite 100" + city: "New York" + state: "NY" + postalCode: "10001" + country: "United States" + mailingAddress: + street: "456 Mail St" + city: "New York" + state: "NY" + postalCode: "10002" + country: "United States" + taxInfo: + taxIdType: EIN + taxIdRef: + secretName: "acme-corp-tax-id" + secretKey: "tax-id" + namespace: "default" + country: "United States" + taxDocument: "W-9" +status: + status: active + verificationStatus: approved + requiredVerifications: 2 + completedVerifications: 2 + pendingVerifications: 0 + rejectedVerifications: 0 + expiredVerifications: 0 + lastVerifiedAt: "2024-01-15T14:30:00Z" + activatedAt: "2024-01-15T15:00:00Z" + conditions: + - type: "Ready" + status: "True" + reason: "VendorActive" + message: "Vendor is active and verified" + lastTransitionTime: "2024-01-15T15:00:00Z" + - type: "Validated" + status: "True" + reason: "ValidationPassed" + message: "All required fields validated" + lastTransitionTime: "2024-01-15T10:00:00Z" + - type: "Verified" + status: "True" + reason: "VerificationComplete" + message: "All required verifications completed" + lastTransitionTime: "2024-01-15T14:30:00Z" + - type: "Active" + status: "True" + reason: "Activated" + message: "Vendor is active and ready for business" + lastTransitionTime: "2024-01-15T15:00:00Z" +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: john-doe-consulting + annotations: + kubernetes.io/display-name: "John Doe Consulting" +spec: + profileType: person + legalName: "John Doe" + nickname: "John" + description: "Independent consultant specializing in cloud architecture" + website: "https://johndoe.example.com" + billingAddress: + street: "789 Home St" + city: "San Francisco" + state: "CA" + postalCode: "94102" + country: "United States" + taxInfo: + taxIdType: SSN + taxIdRef: + secretName: "john-doe-tax-id" + secretKey: "ssn" + namespace: "default" + country: "United States" + taxDocument: "W-9" +status: + status: pending + verificationStatus: in-progress + requiredVerifications: 2 + completedVerifications: 0 + pendingVerifications: 2 + rejectedVerifications: 0 + expiredVerifications: 0 + conditions: + - type: "Ready" + status: "False" + reason: "VerificationPending" + message: "Vendor pending verification completion" + lastTransitionTime: "2024-01-17T11:20:00Z" + - type: "Validated" + status: "True" + reason: "ValidationPassed" + message: "All required fields validated" + lastTransitionTime: "2024-01-17T10:00:00Z" + - type: "Verified" + status: "False" + reason: "VerificationInProgress" + message: "Verification process in progress" + lastTransitionTime: "2024-01-17T11:20:00Z" + - type: "Active" + status: "False" + reason: "NotActivated" + message: "Vendor not yet activated" + lastTransitionTime: "2024-01-17T11:20:00Z" diff --git a/config/samples/vendors/v1alpha1/vendortypedefinition-example.yaml b/config/samples/vendors/v1alpha1/vendortypedefinition-example.yaml new file mode 100644 index 00000000..0997153a --- /dev/null +++ b/config/samples/vendors/v1alpha1/vendortypedefinition-example.yaml @@ -0,0 +1,169 @@ +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: llc + annotations: + kubernetes.io/display-name: "Limited Liability Company (LLC)" +spec: + code: "llc" + displayName: "Limited Liability Company (LLC)" + description: "A business structure that combines the pass-through taxation of a partnership or sole proprietorship with the limited liability of a corporation" + enabled: true + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9", "W-8BEN"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: s-corp + annotations: + kubernetes.io/display-name: "S Corporation" +spec: + code: "s-corp" + displayName: "S Corporation" + description: "A special type of corporation that passes corporate income, losses, deductions, and credits through to shareholders for federal tax purposes" + enabled: true + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: c-corp + annotations: + kubernetes.io/display-name: "C Corporation" +spec: + code: "c-corp" + displayName: "C Corporation" + description: "A legal structure for a corporation in which the owners, or shareholders, are taxed separately from the entity" + enabled: true + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: partnership + annotations: + kubernetes.io/display-name: "Partnership" +spec: + code: "partnership" + displayName: "Partnership" + description: "A business structure in which two or more individuals manage and operate a business in accordance with the terms and objectives set out in a Partnership Deed" + enabled: true + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: sole-proprietorship + annotations: + kubernetes.io/display-name: "Sole Proprietorship" +spec: + code: "sole-proprietorship" + displayName: "Sole Proprietorship" + description: "A business structure in which an individual and their company are considered a single entity for tax and liability purposes" + enabled: true + category: "business" + requiresBusinessFields: false + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: nonprofit + annotations: + kubernetes.io/display-name: "Non-Profit Corporation" +spec: + code: "nonprofit" + displayName: "Non-Profit Corporation" + description: "A corporation that has been granted tax-exempt status by the IRS because it furthers a social cause and provides a public benefit" + enabled: true + category: "nonprofit" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: individual + annotations: + kubernetes.io/display-name: "Individual" +spec: + code: "individual" + displayName: "Individual" + description: "An individual person providing services or goods" + enabled: true + category: "individual" + requiresBusinessFields: false + requiresTaxVerification: true + validCountries: [] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: ltd + annotations: + kubernetes.io/display-name: "Limited Company (Ltd.)" +spec: + code: "ltd" + displayName: "Limited Company (Ltd.)" + description: "A private limited company, common in the UK and other Commonwealth countries" + enabled: true + category: "international" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["GB", "AU", "CA", "NZ"] + requiredTaxDocuments: ["W-8BEN"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: gmbh + annotations: + kubernetes.io/display-name: "GmbH" +spec: + code: "gmbh" + displayName: "Gesellschaft mit beschränkter Haftung (GmbH)" + description: "A type of legal entity in Germany, Austria, and Switzerland" + enabled: true + category: "international" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["DE", "AT", "CH"] + requiredTaxDocuments: ["W-8BEN"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: other + annotations: + kubernetes.io/display-name: "Other" +spec: + code: "other" + displayName: "Other" + description: "Other business structure not listed above" + enabled: true + category: "other" + requiresBusinessFields: false + requiresTaxVerification: true + validCountries: [] + requiredTaxDocuments: ["W-9"] diff --git a/config/samples/vendors/v1alpha1/vendorverification-example.yaml b/config/samples/vendors/v1alpha1/vendorverification-example.yaml new file mode 100644 index 00000000..6b5f3176 --- /dev/null +++ b/config/samples/vendors/v1alpha1/vendorverification-example.yaml @@ -0,0 +1,179 @@ +# Tax verification for ACME Corporation +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: acme-corp-tax-verification + annotations: + kubernetes.io/display-name: "ACME Corp Tax Verification" +spec: + vendorRef: + name: acme-corp + namespace: default + verificationType: tax + status: approved + description: "Tax ID verification for EIN 12-3456789" + documents: + - type: "W-9" + reference: "acme-corp-w9-document" + version: "2024-01-15" + valid: true + - type: "EIN-Confirmation" + reference: "acme-corp-ein-confirmation" + version: "2024-01-15" + valid: true + verifierRef: + type: admin + name: "admin@company.com" + metadata: + department: "Finance" + role: "Tax Specialist" + notes: "Tax ID verified through IRS database and W-9 form validation" + priority: 8 + required: true + expirationDate: "2025-01-15T00:00:00Z" +status: + completedAt: "2024-01-15T14:30:00Z" + lastUpdatedAt: "2024-01-15T14:30:00Z" +--- +# Business verification for ACME Corporation +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: acme-corp-business-verification + annotations: + kubernetes.io/display-name: "ACME Corp Business Verification" +spec: + vendorRef: + name: acme-corp + namespace: default + verificationType: business + status: approved + description: "Business registration and incorporation verification" + documents: + - type: "Articles-of-Incorporation" + reference: "acme-corp-articles" + version: "2023-06-01" + valid: true + - type: "Business-License" + reference: "acme-corp-license" + version: "2024-01-01" + expirationDate: "2024-12-31T00:00:00Z" + valid: true + verifierRef: + type: external-service + name: "business-verification-service" + metadata: + service: "Dun & Bradstreet" + api-version: "v2.1" + notes: "Business verified through D&B database and state records" + priority: 7 + required: true +status: + completedAt: "2024-01-16T09:15:00Z" + lastUpdatedAt: "2024-01-16T09:15:00Z" +--- +# Identity verification for John Doe Consulting +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: john-doe-identity-verification + annotations: + kubernetes.io/display-name: "John Doe Identity Verification" +spec: + vendorRef: + name: john-doe-consulting + namespace: default + verificationType: identity + status: in-progress + description: "Identity verification for individual contractor" + documents: + - type: "Government-ID" + reference: "john-doe-drivers-license" + version: "2024-01-10" + valid: true + - type: "SSN-Verification" + reference: "john-doe-ssn-verification" + version: "2024-01-10" + valid: true + verifierRef: + type: user + name: "verifier@company.com" + metadata: + department: "HR" + role: "Background Check Specialist" + notes: "Pending additional documentation for address verification" + priority: 6 + required: true +status: + lastUpdatedAt: "2024-01-17T11:20:00Z" + lastError: "Address verification document not provided" +--- +# Compliance verification for international vendor +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: international-corp-compliance-verification + annotations: + kubernetes.io/display-name: "International Corp Compliance Verification" +spec: + vendorRef: + name: international-corp + namespace: default + verificationType: compliance + status: pending + description: "International compliance and regulatory verification" + documents: + - type: "VAT-Certificate" + reference: "international-corp-vat-cert" + version: "2024-01-01" + valid: true + - type: "Export-License" + reference: "international-corp-export-license" + version: "2024-01-01" + expirationDate: "2024-12-31T00:00:00Z" + valid: true + verifierRef: + type: external-service + name: "compliance-verification-service" + metadata: + service: "Global Compliance Check" + region: "EU" + notes: "Awaiting additional regulatory documentation" + priority: 9 + required: true + externalReference: "COMP-2024-001234" +status: + lastUpdatedAt: "2024-01-18T08:45:00Z" +--- +# Rejected verification example +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: rejected-vendor-verification + annotations: + kubernetes.io/display-name: "Rejected Vendor Verification" +spec: + vendorRef: + name: rejected-vendor + namespace: default + verificationType: tax + status: rejected + description: "Tax verification failed due to invalid documentation" + documents: + - type: "W-9" + reference: "rejected-vendor-w9" + version: "2024-01-10" + valid: false + verifierRef: + type: admin + name: "admin@company.com" + metadata: + department: "Finance" + role: "Tax Specialist" + notes: "W-9 form contains invalid information and cannot be verified" + priority: 5 + required: true +status: + completedAt: "2024-01-19T16:30:00Z" + lastUpdatedAt: "2024-01-19T16:30:00Z" + lastError: "Invalid tax ID format and missing required fields" diff --git a/docs/api/dynamic-corporation-types.md b/docs/api/dynamic-corporation-types.md new file mode 100644 index 00000000..be2f9be5 --- /dev/null +++ b/docs/api/dynamic-corporation-types.md @@ -0,0 +1,255 @@ +# Dynamic Vendor Types + +This document explains how to manage vendor types dynamically in the Milo system using individual `VendorTypeDefinition` resources. + +## Overview + +Instead of hardcoded vendor types, the system now supports dynamic vendor types that can be managed by staff users through individual Kubernetes resources. This allows for: + +- Adding new vendor types without code changes +- Enabling/disabling individual vendor types +- Customizing display names and descriptions +- Organizing types with sort orders +- Independent lifecycle management for each type +- Rich metadata and validation rules per type + +## Architecture + +### VendorTypeDefinition CRD + +Each vendor type is now a separate `VendorTypeDefinition` resource with its own spec and status: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: llc +spec: + code: "llc" + displayName: "Limited Liability Company (LLC)" + description: "A business structure that combines..." + enabled: true + sortOrder: 10 + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9", "W-8BEN"] +``` + +### Vendor CRD Updates + +The `Vendor` CRD now uses a string field for `vendorType` that references the codes defined in `VendorTypeDefinition` resources: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp +spec: + profileType: business + legalName: "ACME Corporation LLC" + vendorType: "llc" # References code from VendorTypeDefinition + # ... other fields +``` + +## Usage + +### 1. Create Individual Vendor Type Definitions + +Create separate `VendorTypeDefinition` resources for each vendor type: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: llc +spec: + code: "llc" + displayName: "Limited Liability Company (LLC)" + description: "A business structure that combines..." + enabled: true + sortOrder: 10 + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +--- +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: s-corp +spec: + code: "s-corp" + displayName: "S Corporation" + description: "A special type of corporation..." + enabled: true + sortOrder: 20 + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +``` + +### 2. Create Vendors with Dynamic Types + +When creating vendors, use the codes defined in your `VendorTypeDefinition` resources: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: example-vendor +spec: + profileType: business + legalName: "Example Business" + vendorType: "llc" # Must match a code from VendorTypeDefinition + # ... other fields +``` + +### 3. Managing Vendor Types + +#### Adding New Types + +To add a new vendor type, create a new `VendorTypeDefinition` resource: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: new-type +spec: + code: "new-type" + displayName: "New Business Type" + description: "A new type of business structure" + enabled: true + sortOrder: 40 + category: "business" + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US"] + requiredTaxDocuments: ["W-9"] +``` + +#### Disabling Types + +To disable a vendor type, update its `enabled` field: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: old-type +spec: + code: "old-type" + displayName: "Old Business Type" + enabled: false # Disable this type + sortOrder: 50 + # ... other fields +``` + +#### Reordering Types + +Use the `sortOrder` field to control display order (lower numbers appear first): + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: priority-type +spec: + code: "priority-type" + displayName: "Priority Type" + enabled: true + sortOrder: 5 # Will appear first + # ... other fields +``` + +## Validation + +The system validates that: + +1. Vendor type codes must be unique across all `VendorTypeDefinition` resources +2. Vendor type codes must match the pattern `^[a-z0-9-]+$` +3. Vendor `vendorType` values must reference valid, enabled `VendorTypeDefinition` resources +4. Business fields are required when `requiresBusinessFields` is true +5. Tax verification is required when `requiresTaxVerification` is true + +## API Functions + +The system provides helper functions for validation and display: + +```go +// Validate a vendor type against a specific definition +err := ValidateVendorType(vendor.Spec.VendorType, definition) + +// Validate a vendor type against a list of definitions +err := ValidateVendorTypeFromList(vendor.Spec.VendorType, definitions) + +// Get display name for a vendor type +displayName := GetVendorTypeDisplayName(vendor.Spec.VendorType, definition) + +// Get display name from a list of definitions +displayName := GetVendorTypeDisplayNameFromList(vendor.Spec.VendorType, definitions) + +// Get all available vendor types +availableTypes := GetAvailableVendorTypes(definitions) + +// Find a specific vendor type definition +definition := FindVendorTypeDefinition("llc", definitions) +``` + +## Migration from Hardcoded Types + +If you have existing vendors with hardcoded vendor types, you'll need to: + +1. Create individual `VendorTypeDefinition` resources for each type +2. Update existing vendor resources to use the new string codes +3. Remove the old hardcoded enum from the codebase + +## Best Practices + +1. **Use descriptive codes**: Use lowercase, hyphenated codes like `"s-corp"` instead of `"scorp"` +2. **Keep codes stable**: Once vendors are using a code, avoid changing it +3. **Use sort orders**: Organize types logically for UI display +4. **Provide descriptions**: Help users understand what each type means +5. **Test changes**: Always test vendor type changes in a development environment first +6. **Use categories**: Group related types together (business, nonprofit, international, etc.) +7. **Set validation rules**: Use `requiresBusinessFields` and `requiresTaxVerification` appropriately +8. **Specify countries**: Use `validCountries` to restrict types to specific regions + +## Advanced Features + +### Categories + +Use the `category` field to group vendor types: + +- `business` - Standard business entities +- `nonprofit` - Non-profit organizations +- `international` - International business structures +- `individual` - Individual contractors +- `other` - Miscellaneous types + +### Validation Rules + +Each vendor type can specify validation requirements: + +- `requiresBusinessFields` - Whether business-specific fields are required +- `requiresTaxVerification` - Whether tax verification is mandatory +- `validCountries` - Countries where this type is valid +- `requiredTaxDocuments` - Required tax document types + +### Status Tracking + +The status field tracks usage statistics: + +- `vendorCount` - Number of vendors using this type +- `lastUsed` - Last time this type was used + +## Examples + +See the sample files: +- `config/samples/vendors/v1alpha1/vendortypedefinition-example.yaml` +- `config/samples/vendors/v1alpha1/vendor-example.yaml` diff --git a/docs/api/infrastructure.md b/docs/api/infrastructure.md index 2fb6b100..a7e6e792 100644 --- a/docs/api/infrastructure.md +++ b/docs/api/infrastructure.md @@ -3,6 +3,7 @@ Packages: - [infrastructure.miloapis.com/v1alpha1](#infrastructuremiloapiscomv1alpha1) +- [resourcemanager.miloapis.com/v1alpha1](#resourcemanagermiloapiscomv1alpha1) # infrastructure.miloapis.com/v1alpha1 @@ -10,165 +11,84 @@ Resource Types: - [ProjectControlPlane](#projectcontrolplane) +# resourcemanager.miloapis.com/v1alpha1 + +Resource Types: +--- ## ProjectControlPlane [↩ Parent](#infrastructuremiloapiscomv1alpha1 ) - - - - - ProjectControlPlane is the Schema for the projectcontrolplanes API. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringinfrastructure.miloapis.com/v1alpha1true
kindstringProjectControlPlanetrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject - ProjectControlPlaneSpec defines the desired state of ProjectControlPlane.
-
true
statusobject - ProjectControlPlaneStatus defines the observed state of ProjectControlPlane.
-
- Default: map[conditions:[map[lastTransitionTime:1970-01-01T00:00:00Z message:Creating a new control plane for the project reason:Creating status:False type:ControlPlaneReady]]]
-
false
NameTypeDescriptionRequired
apiVersionstringinfrastructure.miloapis.com/v1alpha1true
kindstringProjectControlPlanetrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobjectProjectControlPlaneSpec defines the desired state of ProjectControlPlane.true
statusobjectProjectControlPlaneStatus defines the observed state of ProjectControlPlane.

Default: map[conditions:[map[lastTransitionTime:1970-01-01T00:00:00Z message:Creating a new control plane for the project reason:Creating status:False type:ControlPlaneReady]]]
false
- ### ProjectControlPlane.status [↩ Parent](#projectcontrolplane) - - ProjectControlPlaneStatus defines the observed state of ProjectControlPlane. - - - - - - - - - - - - - - + + + + + + + + +
NameTypeDescriptionRequired
conditions[]object - Represents the observations of a project control plane's current state. -Known condition types are: "Ready"
-
false
NameTypeDescriptionRequired
conditions[]object +Represents the observations of a project control plane's current state. Known condition types are: "Ready"
+
false
- ### ProjectControlPlane.status.conditions[index] [↩ Parent](#projectcontrolplanestatus) - - Condition contains details for one aspect of the current state of this API Resource. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
lastTransitionTimestring - lastTransitionTime is the last time the condition transitioned from one status to another. -This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
-
- Format: date-time
-
true
messagestring - message is a human readable message indicating details about the transition. -This may be an empty string.
-
true
reasonstring - reason contains a programmatic identifier indicating the reason for the condition's last transition. -Producers of specific condition types may define expected values and meanings for this field, -and whether the values are considered a guaranteed API. -The value should be a CamelCase string. -This field may not be empty.
-
true
statusenum - status of the condition, one of True, False, Unknown.
-
- Enum: True, False, Unknown
-
true
typestring - type of condition in CamelCase or in foo.example.com/CamelCase.
-
true
observedGenerationinteger - observedGeneration represents the .metadata.generation that the condition was set based upon. -For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date -with respect to the current state of the instance.
-
- Format: int64
- Minimum: 0
-
false
NameTypeDescriptionRequired
lastTransitionTimestringlastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
Format: date-time
true
messagestringmessage is a human readable message indicating details about the transition. This may be an empty string.
true
reasonstringreason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
true
statusenumstatus of the condition, one of True, False, Unknown.
Enum: True, False, Unknown
true
typestringtype of condition in CamelCase or in foo.example.com/CamelCase.
true
observedGenerationintegerobservedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
Format: int64
Minimum: 0
false
+ +--- + + \ No newline at end of file diff --git a/docs/api/resourcemanager.md b/docs/api/resourcemanager.md index e29d5ab6..3f11db41 100644 --- a/docs/api/resourcemanager.md +++ b/docs/api/resourcemanager.md @@ -1,5 +1,6 @@ # API Reference + Packages: - [resourcemanager.miloapis.com/v1alpha1](#resourcemanagermiloapiscomv1alpha1) @@ -9,22 +10,12 @@ Packages: Resource Types: - [OrganizationMembership](#organizationmembership) - - [Organization](#organization) - - [Project](#project) - - - ## OrganizationMembership [↩ Parent](#resourcemanagermiloapiscomv1alpha1 ) - - - - - OrganizationMembership is the Schema for the organizationmemberships API @@ -70,12 +61,9 @@ OrganizationMembership is the Schema for the organizationmemberships API
- ### OrganizationMembership.spec [↩ Parent](#organizationmembership) - - OrganizationMembershipSpec defines the desired state of OrganizationMembership @@ -104,12 +92,9 @@ OrganizationMembershipSpec defines the desired state of OrganizationMembership
- ### OrganizationMembership.spec.organizationRef [↩ Parent](#organizationmembershipspec) - - OrganizationRef is a reference to the Organization that the user is a member of. @@ -131,12 +116,9 @@ OrganizationRef is a reference to the Organization that the user is a member of.
- ### OrganizationMembership.spec.userRef [↩ Parent](#organizationmembershipspec) - - UserRef is a reference to the User that is a member of the Organization. @@ -158,12 +140,9 @@ UserRef is a reference to the User that is a member of the Organization.
- ### OrganizationMembership.status [↩ Parent](#organizationmembership) - - OrganizationMembershipStatus defines the observed state of OrganizationMembership @@ -210,12 +189,9 @@ OrganizationMembershipStatus defines the observed state of OrganizationMembershi
- ### OrganizationMembership.status.conditions[index] [↩ Parent](#organizationmembershipstatus) - - Condition contains details for one aspect of the current state of this API Resource. @@ -287,12 +263,9 @@ with respect to the current state of the instance.
- ### OrganizationMembership.status.organization [↩ Parent](#organizationmembershipstatus) - - Organization contains information about the organization in the membership. @@ -321,12 +294,9 @@ Organization contains information about the organization in the membership.
- ### OrganizationMembership.status.user [↩ Parent](#organizationmembershipstatus) - - User contains information about the user in the membership. @@ -365,11 +335,6 @@ User contains information about the user in the membership. ## Organization [↩ Parent](#resourcemanagermiloapiscomv1alpha1 ) - - - - - Use lowercase for path, which influences plural name. Ensure kind is Organization. Organization is the Schema for the Organizations API @@ -416,12 +381,9 @@ Organization is the Schema for the Organizations API
- ### Organization.spec [↩ Parent](#organization) - - OrganizationSpec defines the desired state of Organization @@ -446,12 +408,9 @@ OrganizationSpec defines the desired state of Organization
- ### Organization.status [↩ Parent](#organization) - - OrganizationStatus defines the observed state of Organization @@ -485,12 +444,9 @@ Known condition types are: "Ready"
- ### Organization.status.conditions[index] [↩ Parent](#organizationstatus) - - Condition contains details for one aspect of the current state of this API Resource. @@ -565,11 +521,6 @@ with respect to the current state of the instance.
## Project [↩ Parent](#resourcemanagermiloapiscomv1alpha1 ) - - - - - Project is the Schema for the projects API.
@@ -615,12 +566,9 @@ Project is the Schema for the projects API.
- ### Project.spec [↩ Parent](#project) - - ProjectSpec defines the desired state of Project. @@ -643,12 +591,9 @@ resource.
- ### Project.spec.ownerRef [↩ Parent](#projectspec) - - OwnerRef is a reference to the owner of the project. Must be a valid resource. @@ -680,12 +625,9 @@ resource. - ### Project.status [↩ Parent](#project) - - ProjectStatus defines the observed state of Project. @@ -710,12 +652,9 @@ Known condition types are: "Ready"
- ### Project.status.conditions[index] [↩ Parent](#projectstatus) - - Condition contains details for one aspect of the current state of this API Resource. @@ -786,3 +725,4 @@ with respect to the current state of the instance.
false
+ \ No newline at end of file diff --git a/docs/api/vendor-status-conditions.md b/docs/api/vendor-status-conditions.md new file mode 100644 index 00000000..dc25eaf6 --- /dev/null +++ b/docs/api/vendor-status-conditions.md @@ -0,0 +1,303 @@ +# Vendor Status Conditions + +This document explains the status conditions pattern used in the Vendor resource to communicate vendor state and verification status. + +## Overview + +The Vendor resource uses Kubernetes status conditions to provide detailed information about the vendor's current state, verification status, and readiness. This approach provides better observability and follows Kubernetes best practices. + +## Status Structure + +### Vendor Status Fields + +The `VendorStatus` includes both high-level status fields and detailed conditions: + +```yaml +status: + # High-level status + status: active # Overall vendor status + verificationStatus: approved # Overall verification status + + # Verification counts + requiredVerifications: 2 + completedVerifications: 2 + pendingVerifications: 0 + rejectedVerifications: 0 + expiredVerifications: 0 + + # Timestamps + lastVerifiedAt: "2024-01-15T14:30:00Z" + activatedAt: "2024-01-15T15:00:00Z" + rejectedAt: null + rejectionReason: "" + + # Detailed conditions + conditions: + - type: "Ready" + status: "True" + reason: "VendorActive" + message: "Vendor is active and verified" + lastTransitionTime: "2024-01-15T15:00:00Z" + - type: "Validated" + status: "True" + reason: "ValidationPassed" + message: "All required fields validated" + lastTransitionTime: "2024-01-15T10:00:00Z" + - type: "Verified" + status: "True" + reason: "VerificationComplete" + message: "All required verifications completed" + lastTransitionTime: "2024-01-15T14:30:00Z" + - type: "Active" + status: "True" + reason: "Activated" + message: "Vendor is active and ready for business" + lastTransitionTime: "2024-01-15T15:00:00Z" +``` + +## Condition Types + +### 1. Ready +**Purpose**: Overall readiness of the vendor +**Status**: True/False/Unknown +**Reasons**: +- `VendorActive`: Vendor is active and ready for business +- `VendorPending`: Vendor is pending verification or activation +- `VendorRejected`: Vendor has been rejected +- `VendorArchived`: Vendor has been archived + +### 2. Validated +**Purpose**: Whether vendor data validation has passed +**Status**: True/False/Unknown +**Reasons**: +- `ValidationPassed`: All required fields validated successfully +- `ValidationFailed`: Validation failed due to missing or invalid data + +### 3. Verified +**Purpose**: Whether all required verifications are completed +**Status**: True/False/Unknown +**Reasons**: +- `VerificationComplete`: All required verifications completed +- `VerificationInProgress`: Verification process in progress +- `VerificationFailed`: Verification process failed + +### 4. Active +**Purpose**: Whether vendor is active and can conduct business +**Status**: True/False/Unknown +**Reasons**: +- `Activated`: Vendor is active and ready for business +- `NotActivated`: Vendor is not yet activated + +## Status Values + +### Vendor Status +- `pending`: Vendor is pending verification or activation +- `active`: Vendor is active and ready for business +- `rejected`: Vendor has been rejected +- `archived`: Vendor has been archived + +### Verification Status +- `pending`: No verifications started +- `in-progress`: Verifications in progress +- `approved`: All required verifications approved +- `rejected`: Some verifications rejected +- `expired`: Some verifications expired + +## API Functions + +### Status Management + +```go +// Set vendor status and update conditions +SetVendorStatus(vendor, VendorStatusActive, ReasonVendorActive, "Vendor activated") + +// Set validation status +SetValidationStatus(vendor, true, ReasonValidationPassed, "All fields validated") + +// Set verification status +SetVerificationStatus(vendor, VerificationStatusApproved, ReasonVerificationComplete, "All verifications complete") + +// Set active status +SetActiveStatus(vendor, true, ReasonActivated, "Vendor activated") + +// Set individual condition +SetCondition(vendor, ConditionTypeReady, metav1.ConditionTrue, ReasonVendorActive, "Vendor is ready") +``` + +### Status Queries + +```go +// Check condition status +isReady := IsConditionTrue(vendor, ConditionTypeReady) +isValidated := IsConditionTrue(vendor, ConditionTypeValidated) +isVerified := IsConditionTrue(vendor, ConditionTypeVerified) +isActive := IsConditionTrue(vendor, ConditionTypeActive) + +// Get specific condition +condition := GetCondition(vendor, ConditionTypeReady) + +// Check if vendor can be activated +canActivate, reason := CanActivateVendor(vendor) + +// Get status summary +summary := GetVendorStatusSummary(vendor) +``` + +### Verification Integration + +```go +// Update vendor status from verifications +err := UpdateVendorStatusFromVerifications(ctx, client, vendor) + +// Update verification counts +UpdateVerificationCounts(vendor, verifications) +``` + +## Usage Examples + +### Creating a New Vendor + +```go +vendor := &Vendor{ + Spec: VendorSpec{ + ProfileType: VendorProfileTypeBusiness, + LegalName: "ACME Corp", + // ... other fields + }, +} + +// Set initial status +SetVendorStatus(vendor, VendorStatusPending, ReasonVendorPending, "Vendor created, pending verification") +SetValidationStatus(vendor, true, ReasonValidationPassed, "Initial validation passed") +SetVerificationStatus(vendor, VerificationStatusPending, ReasonVerificationInProgress, "Verification pending") +SetActiveStatus(vendor, false, ReasonNotActivated, "Not yet activated") +``` + +### Updating Verification Status + +```go +// When verifications are completed +SetVerificationStatus(vendor, VerificationStatusApproved, ReasonVerificationComplete, "All verifications approved") + +// Update active status +SetActiveStatus(vendor, true, ReasonActivated, "Vendor activated after verification") + +// Update overall status +SetVendorStatus(vendor, VendorStatusActive, ReasonVendorActive, "Vendor is active and ready") +``` + +### Handling Rejection + +```go +// When vendor is rejected +SetVendorStatus(vendor, VendorStatusRejected, ReasonVendorRejected, "Vendor rejected due to failed verification") +vendor.Status.RejectionReason = "Tax verification failed" +vendor.Status.RejectedAt = &metav1.Time{Time: time.Now()} +``` + +## Monitoring and Alerting + +### Key Metrics to Monitor + +1. **Vendor Status Distribution** + - Count of vendors by status (pending, active, rejected, archived) + - Count of vendors by verification status + +2. **Verification Metrics** + - Average time to complete verification + - Verification success rate + - Number of expired verifications + +3. **Condition Health** + - Count of vendors with each condition type + - Condition transition frequency + +### Alerting Rules + +```yaml +# Alert on high rejection rate +- alert: HighVendorRejectionRate + expr: rate(vendor_rejections_total[5m]) > 0.1 + for: 5m + labels: + severity: warning + annotations: + summary: "High vendor rejection rate detected" + +# Alert on expired verifications +- alert: ExpiredVerifications + expr: vendor_expired_verifications > 0 + for: 0m + labels: + severity: warning + annotations: + summary: "Vendors with expired verifications detected" + +# Alert on stuck pending vendors +- alert: StuckPendingVendors + expr: time() - vendor_created_timestamp > 86400 and vendor_status == "pending" + for: 1h + labels: + severity: warning + annotations: + summary: "Vendors stuck in pending status for over 24 hours" +``` + +## Best Practices + +### 1. Condition Management +- Always set conditions with appropriate reasons and messages +- Use consistent reason codes across the system +- Include timestamps for all status changes +- Keep condition messages descriptive and actionable + +### 2. Status Updates +- Update verification counts when verifications change +- Set timestamps for significant status changes +- Maintain consistency between high-level status and conditions +- Use atomic updates to prevent race conditions + +### 3. Monitoring +- Monitor condition transition frequencies +- Set up alerts for stuck or failed states +- Track verification completion times +- Monitor rejection rates and reasons + +### 4. API Design +- Provide clear status summary functions +- Include validation functions for status changes +- Use consistent naming conventions +- Document all condition types and reasons + +## Migration from Simple Status + +If migrating from a simple status field: + +1. **Add status conditions** to existing vendors +2. **Update controllers** to set conditions appropriately +3. **Modify applications** to read from conditions +4. **Add monitoring** for condition health +5. **Test status transitions** end-to-end + +## Integration with Controllers + +Controllers should update vendor status based on their specific concerns: + +```go +func (r *VendorValidationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // ... validation logic ... + + if validationPassed { + SetValidationStatus(vendor, true, ReasonValidationPassed, "Validation completed") + } else { + SetValidationStatus(vendor, false, ReasonValidationFailed, "Validation failed: " + reason) + } + + // Update overall status + UpdateVendorStatusFromVerifications(ctx, r.Client, vendor) + + return ctrl.Result{}, nil +} +``` + +This status conditions approach provides much better observability and follows Kubernetes best practices for communicating resource state! diff --git a/docs/api/vendor-type-definitions.md b/docs/api/vendor-type-definitions.md new file mode 100644 index 00000000..2653f517 --- /dev/null +++ b/docs/api/vendor-type-definitions.md @@ -0,0 +1,197 @@ +# VendorTypeDefinition Architecture + +This document explains the new `VendorTypeDefinition` architecture that replaces the previous `CorporationTypeConfig` approach. + +## Overview + +The new architecture uses individual `VendorTypeDefinition` resources instead of a single configuration resource. This provides better separation of concerns, independent lifecycle management, and more flexible validation rules. + +## Architecture Comparison + +| Aspect | Old (CorporationTypeConfig) | New (VendorTypeDefinition) | +|--------|----------------------------|----------------------------| +| **Resource Type** | Single config resource | Individual definition resources | +| **Management** | Bulk configuration | Per-type management | +| **Lifecycle** | Single resource lifecycle | Independent lifecycles | +| **Validation** | Basic enabled/disabled | Rich validation rules | +| **Metadata** | Limited fields | Comprehensive metadata | +| **Scalability** | Monolithic | Microservice-friendly | + +## Key Benefits + +### 1. **Independent Lifecycle Management** +Each vendor type can be managed independently: +- Enable/disable without affecting other types +- Update individual types without touching others +- Delete unused types without breaking others + +### 2. **Rich Validation Rules** +Each type can have its own validation requirements: +```yaml +spec: + requiresBusinessFields: true + requiresTaxVerification: true + validCountries: ["US", "CA"] + requiredTaxDocuments: ["W-9", "W-8BEN"] +``` + +### 3. **Better Organization** +Types can be categorized and organized: +```yaml +spec: + category: "business" + sortOrder: 10 + displayName: "Limited Liability Company (LLC)" +``` + +### 4. **Usage Tracking** +Each type tracks its own usage statistics: +```yaml +status: + vendorCount: 42 + lastUsed: "2024-01-15T10:30:00Z" +``` + +## Resource Structure + +### VendorTypeDefinition + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: llc +spec: + code: "llc" # Unique identifier + displayName: "LLC" # Human-readable name + description: "..." # Detailed description + enabled: true # Whether type is available + sortOrder: 10 # Display order + category: "business" # Type category + requiresBusinessFields: true # Business fields required + requiresTaxVerification: true # Tax verification required + validCountries: ["US"] # Valid countries + requiredTaxDocuments: ["W-9"] # Required tax docs +status: + observedGeneration: 1 + conditions: + - type: "Ready" + status: "True" + vendorCount: 42 # Usage statistics + lastUsed: "2024-01-15T10:30:00Z" # Last usage +``` + +### Updated Vendor Resource + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp +spec: + profileType: business + legalName: "ACME Corporation LLC" + vendorType: "llc" # References VendorTypeDefinition + # ... other fields +``` + +## Migration Path + +### From CorporationTypeConfig to VendorTypeDefinition + +1. **Create individual definitions** for each type: + ```bash + # Old approach + kubectl get corporationtypeconfigs + + # New approach + kubectl get vendortypedefinitions + ``` + +2. **Update vendor resources** to use new field: + ```yaml + # Old + corporationType: "llc" + + # New + vendorType: "llc" + ``` + +3. **Remove old resources**: + ```bash + kubectl delete corporationtypeconfigs --all + ``` + +## API Functions + +### Validation Functions + +```go +// Validate against specific definition +err := ValidateVendorType(vendor.Spec.VendorType, definition) + +// Validate against list of definitions +err := ValidateVendorTypeFromList(vendor.Spec.VendorType, definitions) +``` + +### Display Functions + +```go +// Get display name +displayName := GetVendorTypeDisplayName(vendor.Spec.VendorType, definition) + +// Get display name from list +displayName := GetVendorTypeDisplayNameFromList(vendor.Spec.VendorType, definitions) +``` + +### Utility Functions + +```go +// Get available types +availableTypes := GetAvailableVendorTypes(definitions) + +// Find specific definition +definition := FindVendorTypeDefinition("llc", definitions) +``` + +## Controller Integration + +The validation controller now works with individual definitions: + +```go +// Get all VendorTypeDefinitions +var definitionList vendorsv1alpha1.VendorTypeDefinitionList +if err := r.List(ctx, &definitionList); err != nil { + return ctrl.Result{}, err +} + +// Validate vendor type +if err := vendorsv1alpha1.ValidateVendorTypeFromList( + vendor.Spec.VendorType, + definitionList.Items, +); err != nil { + return ctrl.Result{}, err +} +``` + +## Best Practices + +1. **Use descriptive resource names** that match the code (e.g., `name: llc` for `code: "llc"`) +2. **Set appropriate categories** to group related types +3. **Use sort orders** to control display order +4. **Set validation rules** based on business requirements +5. **Monitor usage statistics** to identify unused types +6. **Keep codes stable** once vendors are using them + +## Future Enhancements + +The new architecture enables future enhancements: + +1. **Per-type controllers** for business logic +2. **Type-specific webhooks** for validation +3. **Usage analytics** and reporting +4. **Type dependencies** and relationships +5. **Custom validation rules** per type +6. **Type-specific UI components** + +This architecture provides a much more flexible and maintainable approach to managing vendor types in the Milo system. diff --git a/docs/api/vendor-verification.md b/docs/api/vendor-verification.md new file mode 100644 index 00000000..4b09813b --- /dev/null +++ b/docs/api/vendor-verification.md @@ -0,0 +1,339 @@ +# VendorVerification Architecture + +This document explains the `VendorVerification` resource that manages verification processes for vendors, providing better separation of concerns and simpler RBAC. + +## Overview + +The `VendorVerification` resource separates verification concerns from vendor information, allowing for: +- Independent verification lifecycle management +- Granular RBAC permissions +- Rich verification metadata and tracking +- Multiple verification types per vendor +- Audit trails and compliance tracking + +## Architecture Benefits + +### 1. **Separation of Concerns** +- Vendor resource contains only vendor information +- Verification resource handles all verification processes +- Clear boundaries between data and processes + +### 2. **Simplified RBAC** +- Different permissions for vendor data vs verification +- Verification teams can manage verifications without vendor access +- Audit teams can access verification data independently + +### 3. **Rich Verification Metadata** +- Detailed tracking of verification processes +- Document management and validation +- Verifier information and timestamps +- Priority and requirement settings + +## Resource Structure + +### VendorVerification + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: acme-corp-tax-verification +spec: + vendorRef: + name: acme-corp + namespace: default + verificationType: tax + status: approved + description: "Tax ID verification for EIN 12-3456789" + documents: + - type: "W-9" + reference: "acme-corp-w9-document" + version: "2024-01-15" + valid: true + verifierRef: + type: admin + name: "admin@company.com" + metadata: + department: "Finance" + role: "Tax Specialist" + notes: "Tax ID verified through IRS database" + priority: 8 + required: true + expirationDate: "2025-01-15T00:00:00Z" +status: + completedAt: "2024-01-15T14:30:00Z" + lastUpdatedAt: "2024-01-15T14:30:00Z" +``` + +## Verification Types + +### Tax Verification +- **Purpose**: Verify tax identification and compliance +- **Documents**: W-9, W-8BEN, tax certificates +- **Verifiers**: Tax specialists, finance team +- **Priority**: High (required for payment processing) + +### Business Verification +- **Purpose**: Verify business registration and legitimacy +- **Documents**: Articles of incorporation, business licenses +- **Verifiers**: Business verification services, legal team +- **Priority**: High (required for business relationships) + +### Identity Verification +- **Purpose**: Verify individual identity and credentials +- **Documents**: Government ID, background checks +- **Verifiers**: HR team, background check services +- **Priority**: Medium (required for individual contractors) + +### Compliance Verification +- **Purpose**: Verify regulatory compliance +- **Documents**: Compliance certificates, regulatory approvals +- **Verifiers**: Compliance team, legal team +- **Priority**: High (required for regulated industries) + +## Verification Status Lifecycle + +``` +Pending → In-Progress → Approved + ↓ ↓ ↓ +Rejected Expired Expired +``` + +### Status Descriptions + +- **Pending**: Verification created but not yet started +- **In-Progress**: Verification is actively being processed +- **Approved**: Verification completed successfully +- **Rejected**: Verification failed or was denied +- **Expired**: Verification has expired and needs renewal + +## RBAC Examples + +### Verification Team Role +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vendor-verification-manager +rules: +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendorverifications"] + verbs: ["get", "list", "create", "update", "patch"] +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendors"] + verbs: ["get", "list"] # Read-only access to vendor data +``` + +### Vendor Management Role +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vendor-manager +rules: +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendors"] + verbs: ["get", "list", "create", "update", "patch", "delete"] +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendorverifications"] + verbs: ["get", "list"] # Read-only access to verification status +``` + +### Audit Team Role +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vendor-auditor +rules: +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendorverifications"] + verbs: ["get", "list"] # Read-only access for auditing +- apiGroups: ["vendors.miloapis.com"] + resources: ["vendors"] + verbs: ["get", "list"] # Read-only access to vendor data +``` + +## API Functions + +### Basic Operations + +```go +// Get all verifications for a vendor +verifications, err := GetVerificationsForVendor(ctx, client, vendor) + +// Get specific verification type +verification, err := GetVerificationByType(ctx, client, vendor, VerificationTypeTax) + +// Check if vendor is fully verified +isVerified, missing, err := IsVendorVerified(ctx, client, vendor) + +// Get overall verification status +status, err := GetVerificationStatus(ctx, client, vendor) +``` + +### Verification Management + +```go +// Create new verification +err := CreateVerification(ctx, client, vendor, verification) + +// Update verification status +err := UpdateVerificationStatus(ctx, client, verification, VerificationStatusApproved, "Approved by tax specialist") + +// Check for expired verifications +expired, err := GetExpiredVerifications(ctx, client, vendor) + +// Get verification summary +summary, err := GetVerificationSummary(ctx, client, vendor) +``` + +## Usage Examples + +### Creating a Tax Verification + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorVerification +metadata: + name: acme-corp-tax-verification +spec: + vendorRef: + name: acme-corp + verificationType: tax + status: pending + description: "Tax ID verification for EIN 12-3456789" + documents: + - type: "W-9" + reference: "acme-corp-w9-document" + valid: true + verifierRef: + type: admin + name: "admin@company.com" + priority: 8 + required: true +``` + +### Updating Verification Status + +```bash +# Approve verification +kubectl patch vendorverification acme-corp-tax-verification \ + --type='merge' \ + -p='{"spec":{"status":"approved"},"status":{"completedAt":"2024-01-15T14:30:00Z"}}' + +# Reject verification +kubectl patch vendorverification acme-corp-tax-verification \ + --type='merge' \ + -p='{"spec":{"status":"rejected","notes":"Invalid tax ID format"}}' +``` + +### Querying Verification Status + +```bash +# Get all verifications for a vendor +kubectl get vendorverifications -l vendor.miloapis.com/vendor=acme-corp + +# Get pending verifications +kubectl get vendorverifications --field-selector spec.status=pending + +# Get verifications by type +kubectl get vendorverifications --field-selector spec.verificationType=tax +``` + +## Integration with Vendor Status + +The vendor status can be derived from verification status: + +```go +// Check if vendor can be activated +isVerified, missingVerifications, err := IsVendorVerified(ctx, client, vendor) +if err != nil { + return err +} + +if !isVerified { + return fmt.Errorf("vendor cannot be activated, missing verifications: %v", missingVerifications) +} + +// Update vendor status based on verification +if isVerified { + vendor.Spec.Status = VendorStatusActive +} else { + vendor.Spec.Status = VendorStatusPending +} +``` + +## Best Practices + +### 1. **Verification Naming** +Use descriptive names that include vendor and type: +```yaml +metadata: + name: acme-corp-tax-verification + name: john-doe-identity-verification + name: international-corp-compliance-verification +``` + +### 2. **Document References** +Use consistent document reference formats: +```yaml +documents: + - type: "W-9" + reference: "acme-corp-w9-document" + version: "2024-01-15" + - type: "Business-License" + reference: "acme-corp-license-2024" + version: "2024-01-01" +``` + +### 3. **Verifier Information** +Include detailed verifier metadata: +```yaml +verifierRef: + type: admin + name: "admin@company.com" + metadata: + department: "Finance" + role: "Tax Specialist" + employee-id: "EMP-12345" +``` + +### 4. **Priority and Requirements** +Set appropriate priorities and requirements: +```yaml +priority: 8 # High priority (1-10 scale) +required: true # Required for vendor activation +``` + +### 5. **Expiration Management** +Set appropriate expiration dates: +```yaml +expirationDate: "2025-01-15T00:00:00Z" # 1 year from verification +``` + +## Monitoring and Alerting + +### Verification Metrics +- Total verifications by status +- Average verification time +- Expired verifications count +- Verification success rate + +### Alerts +- Verification approaching expiration +- Verification rejected +- Verification stuck in pending status +- Missing required verifications + +## Migration from Embedded Verification + +If you have existing vendors with embedded verification fields: + +1. **Extract verification data** from vendor resources +2. **Create VendorVerification resources** for each verification +3. **Remove verification fields** from vendor resources +4. **Update applications** to use verification resources +5. **Test verification workflows** end-to-end + +This architecture provides a much cleaner separation of concerns and enables more sophisticated verification workflows while maintaining simple RBAC and better audit capabilities. diff --git a/docs/api/vendors-api-group.md b/docs/api/vendors-api-group.md new file mode 100644 index 00000000..7e918b90 --- /dev/null +++ b/docs/api/vendors-api-group.md @@ -0,0 +1,154 @@ +# Vendors API Group + +This document describes the new `vendors.miloapis.com` API group that was created to manage vendor-related resources separately from generic resource management. + +## Overview + +The vendors API group (`vendors.miloapis.com/v1alpha1`) contains all resources related to vendor management, providing a focused and organized approach to handling vendor data and configuration. + +## API Group Structure + +``` +vendors.miloapis.com/v1alpha1/ +├── Vendor # Main vendor resource +├── VendorList # List of vendors +├── CorporationTypeConfig # Configuration for corporation types +└── CorporationTypeConfigList # List of corporation type configs +``` + +## Resources + +### Vendor + +The main resource for managing vendor information. + +**API Version:** `vendors.miloapis.com/v1alpha1` +**Kind:** `Vendor` +**Scope:** `Cluster` + +**Key Features:** +- Support for both person and business profiles +- Comprehensive address management (billing and mailing) +- Secure tax information tracking (stored in Kubernetes Secrets) +- Business-specific fields (vendor type, DBA, etc.) +- Status management (pending, active, rejected, archived) + +### VendorTypeDefinition + +Individual resource for managing each vendor type definition. + +**API Version:** `vendors.miloapis.com/v1alpha1` +**Kind:** `VendorTypeDefinition` +**Scope:** `Cluster` + +**Key Features:** +- Individual vendor type definitions +- Enable/disable types without code changes +- Rich validation rules per type +- Customizable display names and descriptions +- Sort ordering and categorization +- Usage tracking and statistics + +## Migration from ResourceManager + +The vendor-related resources were moved from the `resourcemanager.miloapis.com` API group to the new `vendors.miloapis.com` API group for better organization and separation of concerns. + +### Changes Made + +1. **New API Group Created:** + - `vendors.miloapis.com/v1alpha1` + - Dedicated scheme and registration + - Separate CRD manifests + +2. **Resources Moved:** + - `Vendor` → `vendors.miloapis.com/v1alpha1` + - `CorporationTypeConfig` → `vendors.miloapis.com/v1alpha1` + +3. **Files Updated:** + - Controller manager scheme registration + - Kustomization files + - Sample resources + - Validation controllers + +4. **Old Resources Removed:** + - Vendor types removed from `resourcemanager.miloapis.com` + - Old CRD manifests cleaned up + - Old sample files removed + +## Usage Examples + +### Creating a Vendor + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp +spec: + profileType: business + legalName: "ACME Corporation LLC" + corporationType: llc + status: active + # ... other fields +``` + +### Managing Corporation Types + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: CorporationTypeConfig +metadata: + name: default-corporation-types +spec: + active: true + corporationTypes: + - code: "llc" + displayName: "Limited Liability Company (LLC)" + enabled: true + sortOrder: 10 +``` + +## Benefits of Separate API Group + +1. **Clear Separation of Concerns:** + - Vendors are a distinct business domain + - Separate from generic resource management + - Easier to understand and maintain + +2. **Focused API Management:** + - Dedicated API versioning + - Independent evolution of vendor features + - Clearer RBAC and permissions + +3. **Better Organization:** + - Logical grouping of related resources + - Easier to find and manage vendor-related code + - Cleaner API documentation + +4. **Scalability:** + - Can add vendor-specific features without affecting resource management + - Independent scaling and deployment + - Easier to add vendor-specific controllers and webhooks + +## File Structure + +``` +pkg/apis/vendors/ +├── scheme.go +└── v1alpha1/ + ├── doc.go + ├── register.go + ├── vendor_types.go + ├── corporationtypeconfig_types.go + ├── validation.go + └── zz_generated.deepcopy.go + +config/ +├── crd/bases/vendors/ +│ ├── kustomization.yaml +│ ├── vendors.miloapis.com_vendors.yaml +│ └── vendors.miloapis.com_corporationtypeconfigs.yaml +└── samples/vendors/v1alpha1/ + ├── vendor-example.yaml + └── corporationtypeconfig-example.yaml +``` diff --git a/docs/guides/vendors/tax-id-management.md b/docs/guides/vendors/tax-id-management.md new file mode 100644 index 00000000..70d89aa6 --- /dev/null +++ b/docs/guides/vendors/tax-id-management.md @@ -0,0 +1,196 @@ +# Tax ID Management for Vendors + +This guide explains how to securely manage tax identification numbers for vendors in Milo, including why we use secure storage and how to set it up. + +## Why Secure Tax ID Storage? + +Tax identification numbers (EIN, SSN, VAT, etc.) are highly sensitive information that require special handling: + +- **Legal Compliance**: Tax IDs are subject to strict data protection regulations +- **Security**: These numbers can be used for identity theft and fraud +- **Audit Requirements**: Access to tax IDs must be logged and controlled +- **Separation of Concerns**: Business logic should be separate from sensitive data + +Milo uses secure storage to protect this information while keeping vendor management simple and efficient. + +## How It Works + +Instead of storing tax IDs directly in vendor records, Milo uses a reference system: + +1. **Tax IDs are stored securely** in dedicated secure storage +2. **Vendor records contain only references** to the secure storage +3. **Access is controlled** through role-based permissions +4. **All access is logged** for audit purposes + +## Setting Up Tax ID Storage + +### Step 1: Create a Secure Tax ID Record + +Create a secure record for each vendor's tax ID: + +```bash +# For a US business with EIN +kubectl create tax-id acme-corp-tax-id \ + --vendor=acme-corp \ + --type=ein \ + --value="12-3456789" \ + --country=us + +# For an individual with SSN +kubectl create tax-id john-doe-ssn \ + --vendor=john-doe-consulting \ + --type=ssn \ + --value="123-45-6789" \ + --country=us +``` + +### Step 2: Reference in Vendor Record + +Update your vendor record to reference the secure tax ID: + +```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp +spec: + profileType: business + legalName: "ACME Corporation LLC" + taxInfo: + taxIdType: EIN + taxIdRef: + secretName: "acme-corp-tax-id" + secretKey: "tax-id" + country: "United States" + taxDocument: "W-9" +``` + +## Managing Multiple Tax IDs + +Some vendors may have multiple tax IDs (e.g., international operations): + +### Create a Multi-ID Record + +```bash +# Create a record with multiple tax IDs +kubectl create tax-id complex-vendor-tax-ids \ + --vendor=international-corp \ + --type=multiple \ + --values="ein:12-3456789,vat:GB123456789,business-number:CA123456789" +``` + +### Reference Specific Tax ID + +```yaml +# Reference the US EIN +taxIdRef: + secretName: "complex-vendor-tax-ids" + secretKey: "ein" + +# Or reference the EU VAT +taxIdRef: + secretName: "complex-vendor-tax-ids" + secretKey: "vat" +``` + +## Best Practices + +### Naming Conventions + +Use descriptive names that include the vendor name: + +```bash +# Good examples +acme-corp-tax-id +john-doe-ssn +international-corp-vat + +# Avoid generic names +tax-secret-1 +secret-abc +``` + +### Key Naming + +Use consistent key names within records: + +```yaml +# Standard keys +tax-id # For EIN +ssn # For Social Security Number +vat-number # For VAT numbers +business-number # For business registration numbers +``` + +## Common Operations + +### Viewing Tax ID Information + +```bash +# List all tax ID records for a vendor +kubctl get tax-ids --vendor=acme-corp + +# View specific tax ID (requires appropriate permissions) +kubctl describe tax-id acme-corp-tax-id +``` + +### Updating Tax IDs + +```bash +# Update an existing tax ID +kubctl update tax-id acme-corp-tax-id \ + --value="98-7654321" + +# Add additional tax ID to existing record +kubctl update tax-id complex-vendor-tax-ids \ + --add-key="gst:IN123456789" +``` + +### Removing Tax IDs + +```bash +# Remove a tax ID record +kubctl delete tax-id acme-corp-tax-id + +# Remove specific key from multi-ID record +kubctl update tax-id complex-vendor-tax-ids \ + --remove-key="vat" +``` + +## Troubleshooting + +### Common Issues + +**Tax ID Not Found** +``` +Error: Tax ID record 'acme-corp-tax-id' not found +``` +- Check the record name in your vendor configuration +- Verify the record exists: `kubctl get tax-ids --vendor=acme-corp` + +**Access Denied** +``` +Error: Access denied to tax ID record +``` +- Check your permissions for tax ID access +- Contact your administrator for access + +**Invalid Reference** +``` +Error: Invalid tax ID reference in vendor record +``` +- Verify the `secretName` and `secretKey` in your vendor configuration +- Check that the referenced record exists + +### Getting Help + +```bash +# Check vendor tax ID configuration +kubctl describe vendor acme-corp --show-tax-info + +# Verify tax ID record exists +kubctl get tax-ids --vendor=acme-corp + +# Check your permissions +kubctl auth can-i get tax-ids +``` diff --git a/internal/controllers/resourcemanager/vendor_validation_controller.go b/internal/controllers/resourcemanager/vendor_validation_controller.go new file mode 100644 index 00000000..12954ee6 --- /dev/null +++ b/internal/controllers/resourcemanager/vendor_validation_controller.go @@ -0,0 +1,74 @@ +package resourcemanager + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + vendorscontrollers "go.miloapis.com/milo/internal/controllers/vendors" + vendorsv1alpha1 "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" +) + +// VendorValidationReconciler reconciles Vendor objects to validate corporation types +type VendorValidationReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=vendors.miloapis.com,resources=vendors,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=vendors.miloapis.com,resources=vendors/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=vendors.miloapis.com,resources=vendortypedefinitions,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile validates vendor types against VendorTypeDefinition +func (r *VendorValidationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // Fetch the Vendor + var vendor vendorsv1alpha1.Vendor + if err := r.Get(ctx, req.NamespacedName, &vendor); err != nil { + logger.Error(err, "unable to fetch Vendor") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Skip validation if vendorType is not set + if vendor.Spec.VendorType == "" { + return ctrl.Result{}, nil + } + + // Get all VendorTypeDefinitions + var definitionList vendorsv1alpha1.VendorTypeDefinitionList + if err := r.List(ctx, &definitionList); err != nil { + logger.Error(err, "unable to list VendorTypeDefinitions") + return ctrl.Result{}, err + } + + // Validate the vendor type + if err := vendorsv1alpha1.ValidateVendorTypeFromList(vendor.Spec.VendorType, definitionList.Items); err != nil { + logger.Error(err, "invalid vendor type", "vendorType", vendor.Spec.VendorType) + + // Update vendor status with validation error + // This is a simplified example - in practice you'd want more sophisticated status management + return ctrl.Result{}, fmt.Errorf("invalid vendor type %q: %w", vendor.Spec.VendorType, err) + } + + // Validate tax ID secret reference + if err := vendorscontrollers.ValidateTaxIdSecret(ctx, r.Client, &vendor, vendor.Spec.TaxInfo.TaxIdRef); err != nil { + logger.Error(err, "invalid tax ID secret reference", "secretName", vendor.Spec.TaxInfo.TaxIdRef.SecretName) + return ctrl.Result{}, fmt.Errorf("invalid tax ID secret reference: %w", err) + } + + logger.Info("vendor validated successfully", "vendorType", vendor.Spec.VendorType) + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *VendorValidationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&vendorsv1alpha1.Vendor{}). + Complete(r) +} diff --git a/internal/controllers/vendors/status_utils.go b/internal/controllers/vendors/status_utils.go new file mode 100644 index 00000000..d8a21d19 --- /dev/null +++ b/internal/controllers/vendors/status_utils.go @@ -0,0 +1,286 @@ +package vendors + +import ( + "context" + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vendorsv1alpha1 "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" +) + +// Condition types for Vendor status +const ( + ConditionTypeReady = "Ready" + ConditionTypeValidated = "Validated" + ConditionTypeVerified = "Verified" + ConditionTypeActive = "Active" +) + +// Condition reasons +const ( + ReasonVendorActive = "VendorActive" + ReasonVendorPending = "VendorPending" + ReasonVendorRejected = "VendorRejected" + ReasonVendorArchived = "VendorArchived" + ReasonValidationPassed = "ValidationPassed" + ReasonValidationFailed = "ValidationFailed" + ReasonVerificationComplete = "VerificationComplete" + ReasonVerificationInProgress = "VerificationInProgress" + ReasonVerificationFailed = "VerificationFailed" + ReasonActivated = "Activated" + ReasonNotActivated = "NotActivated" +) + +// SetVendorStatus sets the vendor status and updates related conditions +func SetVendorStatus(vendor *vendorsv1alpha1.Vendor, status vendorsv1alpha1.VendorStatusValue, reason, message string) { + vendor.Status.Status = status + now := metav1.Now() + + // Update Ready condition based on status + readyStatus := metav1.ConditionFalse + readyReason := reason + readyMessage := message + + switch status { + case vendorsv1alpha1.VendorStatusActive: + readyStatus = metav1.ConditionTrue + readyReason = ReasonVendorActive + readyMessage = "Vendor is active and ready for business" + vendor.Status.ActivatedAt = &now + case vendorsv1alpha1.VendorStatusRejected: + readyStatus = metav1.ConditionFalse + readyReason = ReasonVendorRejected + readyMessage = "Vendor has been rejected" + vendor.Status.RejectedAt = &now + case vendorsv1alpha1.VendorStatusArchived: + readyStatus = metav1.ConditionFalse + readyReason = ReasonVendorArchived + readyMessage = "Vendor has been archived" + case vendorsv1alpha1.VendorStatusPending: + readyStatus = metav1.ConditionFalse + readyReason = ReasonVendorPending + readyMessage = "Vendor is pending verification or activation" + } + + SetCondition(vendor, ConditionTypeReady, readyStatus, readyReason, readyMessage) +} + +// SetValidationStatus sets the validation condition +func SetValidationStatus(vendor *vendorsv1alpha1.Vendor, passed bool, reason, message string) { + status := metav1.ConditionFalse + if passed { + status = metav1.ConditionTrue + } + SetCondition(vendor, ConditionTypeValidated, status, reason, message) +} + +// SetVerificationStatus sets the verification condition and updates verification status +func SetVerificationStatus(vendor *vendorsv1alpha1.Vendor, verificationStatus vendorsv1alpha1.VerificationStatus, reason, message string) { + vendor.Status.VerificationStatus = verificationStatus + + status := metav1.ConditionFalse + switch verificationStatus { + case vendorsv1alpha1.VerificationStatusApproved: + status = metav1.ConditionTrue + vendor.Status.LastVerifiedAt = &metav1.Time{Time: time.Now()} + case vendorsv1alpha1.VerificationStatusRejected: + status = metav1.ConditionFalse + case vendorsv1alpha1.VerificationStatusInProgress: + status = metav1.ConditionFalse + case vendorsv1alpha1.VerificationStatusExpired: + status = metav1.ConditionFalse + case vendorsv1alpha1.VerificationStatusPending: + status = metav1.ConditionFalse + } + + SetCondition(vendor, ConditionTypeVerified, status, reason, message) +} + +// SetActiveStatus sets the active condition +func SetActiveStatus(vendor *vendorsv1alpha1.Vendor, active bool, reason, message string) { + status := metav1.ConditionFalse + if active { + status = metav1.ConditionTrue + } + SetCondition(vendor, ConditionTypeActive, status, reason, message) +} + +// SetCondition sets a condition on the vendor status +func SetCondition(vendor *vendorsv1alpha1.Vendor, conditionType string, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + condition := metav1.Condition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: now, + } + + // Find existing condition and update or add new one + for i, existing := range vendor.Status.Conditions { + if existing.Type == conditionType { + if existing.Status != status || existing.Reason != reason { + condition.LastTransitionTime = now + } else { + condition.LastTransitionTime = existing.LastTransitionTime + } + vendor.Status.Conditions[i] = condition + return + } + } + + // Add new condition + vendor.Status.Conditions = append(vendor.Status.Conditions, condition) +} + +// GetCondition returns the condition with the given type +func GetCondition(vendor *vendorsv1alpha1.Vendor, conditionType string) *metav1.Condition { + for i := range vendor.Status.Conditions { + if vendor.Status.Conditions[i].Type == conditionType { + return &vendor.Status.Conditions[i] + } + } + return nil +} + +// IsConditionTrue returns true if the condition with the given type is True +func IsConditionTrue(vendor *vendorsv1alpha1.Vendor, conditionType string) bool { + condition := GetCondition(vendor, conditionType) + return condition != nil && condition.Status == metav1.ConditionTrue +} + +// IsConditionFalse returns true if the condition with the given type is False +func IsConditionFalse(vendor *vendorsv1alpha1.Vendor, conditionType string) bool { + condition := GetCondition(vendor, conditionType) + return condition != nil && condition.Status == metav1.ConditionFalse +} + +// UpdateVerificationCounts updates the verification counts in vendor status +func UpdateVerificationCounts(vendor *vendorsv1alpha1.Vendor, verifications []vendorsv1alpha1.VendorVerification) { + vendor.Status.RequiredVerifications = 0 + vendor.Status.CompletedVerifications = 0 + vendor.Status.PendingVerifications = 0 + vendor.Status.RejectedVerifications = 0 + vendor.Status.ExpiredVerifications = 0 + + for _, verification := range verifications { + if verification.Spec.Required { + vendor.Status.RequiredVerifications++ + } + + switch verification.Spec.Status { + case vendorsv1alpha1.VerificationStatusApproved: + vendor.Status.CompletedVerifications++ + case vendorsv1alpha1.VerificationStatusPending: + vendor.Status.PendingVerifications++ + case vendorsv1alpha1.VerificationStatusRejected: + vendor.Status.RejectedVerifications++ + } + + if IsVerificationExpired(&verification) { + vendor.Status.ExpiredVerifications++ + } + } +} + +// UpdateVendorStatusFromVerifications updates vendor status based on verification status +func UpdateVendorStatusFromVerifications(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) error { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return fmt.Errorf("failed to get verifications: %w", err) + } + + // Update verification counts + UpdateVerificationCounts(vendor, verifications) + + // Determine overall verification status + verificationStatus, err := GetVerificationStatus(ctx, c, vendor) + if err != nil { + return fmt.Errorf("failed to get verification status: %w", err) + } + + // Update verification condition + SetVerificationStatus(vendor, verificationStatus, ReasonVerificationInProgress, "Verification status updated") + + // Update active condition based on verification status + active := verificationStatus == vendorsv1alpha1.VerificationStatusApproved + SetActiveStatus(vendor, active, ReasonActivated, "Vendor activation status updated") + + // Update overall vendor status + if active && vendor.Status.Status != vendorsv1alpha1.VendorStatusActive { + SetVendorStatus(vendor, vendorsv1alpha1.VendorStatusActive, ReasonVendorActive, "Vendor activated after verification completion") + } else if !active && vendor.Status.Status == vendorsv1alpha1.VendorStatusActive { + SetVendorStatus(vendor, vendorsv1alpha1.VendorStatusPending, ReasonVendorPending, "Vendor deactivated due to verification issues") + } + + return nil +} + +// CanActivateVendor checks if a vendor can be activated +func CanActivateVendor(vendor *vendorsv1alpha1.Vendor) (bool, string) { + // Check if all required verifications are completed + if vendor.Status.RequiredVerifications > 0 && vendor.Status.CompletedVerifications < vendor.Status.RequiredVerifications { + return false, fmt.Sprintf("Required verifications incomplete: %d/%d completed", + vendor.Status.CompletedVerifications, vendor.Status.RequiredVerifications) + } + + // Check if there are any rejected verifications + if vendor.Status.RejectedVerifications > 0 { + return false, fmt.Sprintf("Vendor has %d rejected verifications", vendor.Status.RejectedVerifications) + } + + // Check if there are any expired verifications + if vendor.Status.ExpiredVerifications > 0 { + return false, fmt.Sprintf("Vendor has %d expired verifications", vendor.Status.ExpiredVerifications) + } + + // Check if validation passed + if !IsConditionTrue(vendor, ConditionTypeValidated) { + return false, "Vendor validation not passed" + } + + return true, "Vendor can be activated" +} + +// GetVendorStatusSummary returns a summary of the vendor status +func GetVendorStatusSummary(vendor *vendorsv1alpha1.Vendor) *VendorStatusSummary { + return &VendorStatusSummary{ + Status: vendor.Status.Status, + VerificationStatus: vendor.Status.VerificationStatus, + RequiredVerifications: vendor.Status.RequiredVerifications, + CompletedVerifications: vendor.Status.CompletedVerifications, + PendingVerifications: vendor.Status.PendingVerifications, + RejectedVerifications: vendor.Status.RejectedVerifications, + ExpiredVerifications: vendor.Status.ExpiredVerifications, + IsReady: IsConditionTrue(vendor, ConditionTypeReady), + IsValidated: IsConditionTrue(vendor, ConditionTypeValidated), + IsVerified: IsConditionTrue(vendor, ConditionTypeVerified), + IsActive: IsConditionTrue(vendor, ConditionTypeActive), + LastVerifiedAt: vendor.Status.LastVerifiedAt, + ActivatedAt: vendor.Status.ActivatedAt, + RejectedAt: vendor.Status.RejectedAt, + RejectionReason: vendor.Status.RejectionReason, + } +} + +// VendorStatusSummary provides a summary of vendor status +type VendorStatusSummary struct { + Status vendorsv1alpha1.VendorStatusValue `json:"status"` + VerificationStatus vendorsv1alpha1.VerificationStatus `json:"verificationStatus"` + RequiredVerifications int32 `json:"requiredVerifications"` + CompletedVerifications int32 `json:"completedVerifications"` + PendingVerifications int32 `json:"pendingVerifications"` + RejectedVerifications int32 `json:"rejectedVerifications"` + ExpiredVerifications int32 `json:"expiredVerifications"` + IsReady bool `json:"isReady"` + IsValidated bool `json:"isValidated"` + IsVerified bool `json:"isVerified"` + IsActive bool `json:"isActive"` + LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` + ActivatedAt *metav1.Time `json:"activatedAt,omitempty"` + RejectedAt *metav1.Time `json:"rejectedAt,omitempty"` + RejectionReason string `json:"rejectionReason,omitempty"` +} diff --git a/internal/controllers/vendors/taxid_utils.go b/internal/controllers/vendors/taxid_utils.go new file mode 100644 index 00000000..ee133373 --- /dev/null +++ b/internal/controllers/vendors/taxid_utils.go @@ -0,0 +1,165 @@ +package vendors + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + vendorsv1alpha1 "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" +) + +// GetTaxIdFromSecret retrieves the tax ID from the referenced Secret +func GetTaxIdFromSecret(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, taxIdRef vendorsv1alpha1.TaxIdReference) (string, error) { + // Determine the namespace for the Secret + namespace := taxIdRef.Namespace + if namespace == "" { + namespace = vendor.Namespace + if namespace == "" { + namespace = "default" // fallback for cluster-scoped resources + } + } + + // Get the Secret + secret := &corev1.Secret{} + secretKey := types.NamespacedName{ + Name: taxIdRef.SecretName, + Namespace: namespace, + } + + if err := c.Get(ctx, secretKey, secret); err != nil { + return "", fmt.Errorf("failed to get secret %s/%s: %w", namespace, taxIdRef.SecretName, err) + } + + // Extract the tax ID from the Secret + taxIdBytes, exists := secret.Data[taxIdRef.SecretKey] + if !exists { + return "", fmt.Errorf("key %s not found in secret %s/%s", taxIdRef.SecretKey, namespace, taxIdRef.SecretName) + } + + return string(taxIdBytes), nil +} + +// ValidateTaxIdSecret validates that the referenced Secret exists and contains the expected key +func ValidateTaxIdSecret(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, taxIdRef vendorsv1alpha1.TaxIdReference) error { + // Determine the namespace for the Secret + namespace := taxIdRef.Namespace + if namespace == "" { + namespace = vendor.Namespace + if namespace == "" { + namespace = "default" // fallback for cluster-scoped resources + } + } + + // Get the Secret + secret := &corev1.Secret{} + secretKey := types.NamespacedName{ + Name: taxIdRef.SecretName, + Namespace: namespace, + } + + if err := c.Get(ctx, secretKey, secret); err != nil { + return fmt.Errorf("failed to get secret %s/%s: %w", namespace, taxIdRef.SecretName, err) + } + + // Check if the key exists + if _, exists := secret.Data[taxIdRef.SecretKey]; !exists { + return fmt.Errorf("key %s not found in secret %s/%s", taxIdRef.SecretKey, namespace, taxIdRef.SecretName) + } + + return nil +} + +// CreateTaxIdSecret creates a Secret containing the tax ID +func CreateTaxIdSecret(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, taxIdRef vendorsv1alpha1.TaxIdReference, taxId string) error { + // Determine the namespace for the Secret + namespace := taxIdRef.Namespace + if namespace == "" { + namespace = vendor.Namespace + if namespace == "" { + namespace = "default" // fallback for cluster-scoped resources + } + } + + // Create the Secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: taxIdRef.SecretName, + Namespace: namespace, + Labels: map[string]string{ + "vendor.miloapis.com/vendor": vendor.Name, + "vendor.miloapis.com/type": "tax-id", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + taxIdRef.SecretKey: []byte(taxId), + }, + } + + // Set owner reference to the vendor + secret.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: vendor.APIVersion, + Kind: vendor.Kind, + Name: vendor.Name, + UID: vendor.UID, + Controller: &[]bool{true}[0], + }, + } + + return c.Create(ctx, secret) +} + +// UpdateTaxIdSecret updates an existing Secret containing the tax ID +func UpdateTaxIdSecret(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, taxIdRef vendorsv1alpha1.TaxIdReference, taxId string) error { + // Determine the namespace for the Secret + namespace := taxIdRef.Namespace + if namespace == "" { + namespace = vendor.Namespace + if namespace == "" { + namespace = "default" // fallback for cluster-scoped resources + } + } + + // Get the existing Secret + secret := &corev1.Secret{} + secretKey := types.NamespacedName{ + Name: taxIdRef.SecretName, + Namespace: namespace, + } + + if err := c.Get(ctx, secretKey, secret); err != nil { + return fmt.Errorf("failed to get secret %s/%s: %w", namespace, taxIdRef.SecretName, err) + } + + // Update the tax ID + secret.Data[taxIdRef.SecretKey] = []byte(taxId) + + return c.Update(ctx, secret) +} + +// DeleteTaxIdSecret deletes the Secret containing the tax ID +func DeleteTaxIdSecret(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, taxIdRef vendorsv1alpha1.TaxIdReference) error { + // Determine the namespace for the Secret + namespace := taxIdRef.Namespace + if namespace == "" { + namespace = vendor.Namespace + if namespace == "" { + namespace = "default" // fallback for cluster-scoped resources + } + } + + // Delete the Secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: taxIdRef.SecretName, + Namespace: namespace, + }, + } + + return c.Delete(ctx, secret) +} diff --git a/internal/controllers/vendors/verification_utils.go b/internal/controllers/vendors/verification_utils.go new file mode 100644 index 00000000..124d8e9b --- /dev/null +++ b/internal/controllers/vendors/verification_utils.go @@ -0,0 +1,254 @@ +package vendors + +import ( + "context" + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vendorsv1alpha1 "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" +) + +// GetVerificationsForVendor retrieves all verifications for a specific vendor +func GetVerificationsForVendor(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) ([]vendorsv1alpha1.VendorVerification, error) { + var verificationList vendorsv1alpha1.VendorVerificationList + + // Determine the namespace for the search + namespace := vendor.Namespace + if namespace == "" { + namespace = "default" + } + + // List all verifications + if err := c.List(ctx, &verificationList); err != nil { + return nil, fmt.Errorf("failed to list verifications: %w", err) + } + + // Filter verifications for this vendor + var vendorVerifications []vendorsv1alpha1.VendorVerification + for _, verification := range verificationList.Items { + if verification.Spec.VendorRef.Name == vendor.Name { + // Check namespace match (if specified in verification) + if verification.Spec.VendorRef.Namespace == "" || verification.Spec.VendorRef.Namespace == namespace { + vendorVerifications = append(vendorVerifications, verification) + } + } + } + + return vendorVerifications, nil +} + +// GetVerificationByType retrieves a specific verification type for a vendor +func GetVerificationByType(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, verificationType vendorsv1alpha1.VerificationType) (*vendorsv1alpha1.VendorVerification, error) { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return nil, err + } + + for _, verification := range verifications { + if verification.Spec.VerificationType == verificationType { + return &verification, nil + } + } + + return nil, fmt.Errorf("verification of type %s not found for vendor %s", verificationType, vendor.Name) +} + +// IsVendorVerified checks if a vendor has all required verifications approved +func IsVendorVerified(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) (bool, []string, error) { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return false, nil, err + } + + var missingVerifications []string + allVerified := true + + for _, verification := range verifications { + if verification.Spec.Required { + if verification.Spec.Status != vendorsv1alpha1.VerificationStatusApproved { + allVerified = false + missingVerifications = append(missingVerifications, string(verification.Spec.VerificationType)) + } + } + } + + return allVerified, missingVerifications, nil +} + +// GetVerificationStatus returns the overall verification status for a vendor +func GetVerificationStatus(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) (vendorsv1alpha1.VerificationStatus, error) { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return vendorsv1alpha1.VerificationStatusPending, err + } + + if len(verifications) == 0 { + return vendorsv1alpha1.VerificationStatusPending, nil + } + + // Check if any verification is in progress + for _, verification := range verifications { + if verification.Spec.Status == vendorsv1alpha1.VerificationStatusInProgress { + return vendorsv1alpha1.VerificationStatusInProgress, nil + } + } + + // Check if any required verification is rejected + for _, verification := range verifications { + if verification.Spec.Required && verification.Spec.Status == vendorsv1alpha1.VerificationStatusRejected { + return vendorsv1alpha1.VerificationStatusRejected, nil + } + } + + // Check if all required verifications are approved + allApproved := true + for _, verification := range verifications { + if verification.Spec.Required && verification.Spec.Status != vendorsv1alpha1.VerificationStatusApproved { + allApproved = false + break + } + } + + if allApproved { + return vendorsv1alpha1.VerificationStatusApproved, nil + } + + return vendorsv1alpha1.VerificationStatusPending, nil +} + +// IsVerificationExpired checks if a verification has expired +func IsVerificationExpired(verification *vendorsv1alpha1.VendorVerification) bool { + if verification.Spec.ExpirationDate == nil { + return false + } + return verification.Spec.ExpirationDate.Time.Before(time.Now()) +} + +// GetExpiredVerifications returns all expired verifications for a vendor +func GetExpiredVerifications(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) ([]vendorsv1alpha1.VendorVerification, error) { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return nil, err + } + + var expiredVerifications []vendorsv1alpha1.VendorVerification + for _, verification := range verifications { + if IsVerificationExpired(&verification) { + expiredVerifications = append(expiredVerifications, verification) + } + } + + return expiredVerifications, nil +} + +// CreateVerification creates a new verification for a vendor +func CreateVerification(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor, verification *vendorsv1alpha1.VendorVerification) error { + // Set vendor reference + verification.Spec.VendorRef = vendorsv1alpha1.VendorReference{ + Name: vendor.Name, + Namespace: vendor.Namespace, + } + + // Set creation timestamp + now := metav1.Now() + verification.CreationTimestamp = now + + // Set initial status if not set + if verification.Spec.Status == "" { + verification.Spec.Status = vendorsv1alpha1.VerificationStatusPending + } + + // Set owner reference + verification.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: vendor.APIVersion, + Kind: vendor.Kind, + Name: vendor.Name, + UID: vendor.UID, + Controller: &[]bool{false}[0], // Not a controller, just a reference + }, + } + + return c.Create(ctx, verification) +} + +// UpdateVerificationStatus updates the status of a verification +func UpdateVerificationStatus(ctx context.Context, c client.Client, verification *vendorsv1alpha1.VendorVerification, status vendorsv1alpha1.VerificationStatus, notes string) error { + verification.Spec.Status = status + verification.Spec.Notes = notes + + // Update status timestamps + now := metav1.Now() + verification.Status.LastUpdatedAt = &now + + if status == vendorsv1alpha1.VerificationStatusApproved || status == vendorsv1alpha1.VerificationStatusRejected { + verification.Status.CompletedAt = &now + } + + return c.Update(ctx, verification) +} + +// GetVerificationSummary returns a summary of verifications for a vendor +func GetVerificationSummary(ctx context.Context, c client.Client, vendor *vendorsv1alpha1.Vendor) (*VerificationSummary, error) { + verifications, err := GetVerificationsForVendor(ctx, c, vendor) + if err != nil { + return nil, err + } + + summary := &VerificationSummary{ + VendorName: vendor.Name, + TotalVerifications: len(verifications), + RequiredVerifications: 0, + ApprovedVerifications: 0, + PendingVerifications: 0, + RejectedVerifications: 0, + ExpiredVerifications: 0, + VerificationTypes: make(map[vendorsv1alpha1.VerificationType]int), + } + + for _, verification := range verifications { + if verification.Spec.Required { + summary.RequiredVerifications++ + } + + summary.VerificationTypes[verification.Spec.VerificationType]++ + + switch verification.Spec.Status { + case vendorsv1alpha1.VerificationStatusApproved: + summary.ApprovedVerifications++ + case vendorsv1alpha1.VerificationStatusPending: + summary.PendingVerifications++ + case vendorsv1alpha1.VerificationStatusRejected: + summary.RejectedVerifications++ + } + + if IsVerificationExpired(&verification) { + summary.ExpiredVerifications++ + } + } + + // Calculate overall status + overallStatus, err := GetVerificationStatus(ctx, c, vendor) + if err != nil { + return nil, err + } + summary.OverallStatus = overallStatus + + return summary, nil +} + +// VerificationSummary provides a summary of verification status for a vendor +type VerificationSummary struct { + VendorName string `json:"vendorName"` + TotalVerifications int `json:"totalVerifications"` + RequiredVerifications int `json:"requiredVerifications"` + ApprovedVerifications int `json:"approvedVerifications"` + PendingVerifications int `json:"pendingVerifications"` + RejectedVerifications int `json:"rejectedVerifications"` + ExpiredVerifications int `json:"expiredVerifications"` + OverallStatus vendorsv1alpha1.VerificationStatus `json:"overallStatus"` + VerificationTypes map[vendorsv1alpha1.VerificationType]int `json:"verificationTypes"` +} diff --git a/pkg/apis/vendors/scheme.go b/pkg/apis/vendors/scheme.go new file mode 100644 index 00000000..f8b35a16 --- /dev/null +++ b/pkg/apis/vendors/scheme.go @@ -0,0 +1,11 @@ +package vendors + +import ( + "k8s.io/apimachinery/pkg/runtime" + + "go.miloapis.com/milo/pkg/apis/vendors/v1alpha1" +) + +func Install(scheme *runtime.Scheme) { + v1alpha1.AddToScheme(scheme) +} diff --git a/pkg/apis/vendors/v1alpha1/doc.go b/pkg/apis/vendors/v1alpha1/doc.go new file mode 100644 index 00000000..89cb85d2 --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package +// +groupName=vendors.miloapis.com + +// Package v1alpha1 contains API Schema definitions for the vendors v1alpha1 API group +package v1alpha1 diff --git a/pkg/apis/vendors/v1alpha1/register.go b/pkg/apis/vendors/v1alpha1/register.go new file mode 100644 index 00000000..f268272e --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/register.go @@ -0,0 +1,44 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "vendors.miloapis.com", Version: "v1alpha1"} + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that registers this API group & version to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return GroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return GroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &Vendor{}, + &VendorList{}, + &VendorTypeDefinition{}, + &VendorTypeDefinitionList{}, + &VendorVerification{}, + &VendorVerificationList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} + +func init() { + SchemeBuilder.Register(SchemeBuilder...) +} diff --git a/pkg/apis/vendors/v1alpha1/validation.go b/pkg/apis/vendors/v1alpha1/validation.go new file mode 100644 index 00000000..c9ddb474 --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/validation.go @@ -0,0 +1,106 @@ +package v1alpha1 + +import ( + "fmt" + "strings" +) + +// ValidateVendorType validates that a vendor type is valid according to the provided definition +func ValidateVendorType(vendorType VendorType, definition *VendorTypeDefinition) error { + if definition == nil { + return fmt.Errorf("vendor type definition not found") + } + + vendorTypeStr := string(vendorType) + if vendorTypeStr == "" { + return nil // Optional field + } + + if definition.Spec.Code != vendorTypeStr { + return fmt.Errorf("vendor type %q does not match definition code %q", vendorTypeStr, definition.Spec.Code) + } + + if !definition.Spec.Enabled { + return fmt.Errorf("vendor type %q is disabled", vendorTypeStr) + } + + return nil +} + +// ValidateVendorTypeFromList validates that a vendor type is valid according to a list of definitions +func ValidateVendorTypeFromList(vendorType VendorType, definitions []VendorTypeDefinition) error { + vendorTypeStr := string(vendorType) + if vendorTypeStr == "" { + return nil // Optional field + } + + for _, def := range definitions { + if def.Spec.Code == vendorTypeStr { + if !def.Spec.Enabled { + return fmt.Errorf("vendor type %q is disabled", vendorTypeStr) + } + return nil + } + } + + enabledTypes := getEnabledVendorTypes(definitions) + return fmt.Errorf("invalid vendor type %q, must be one of: %s", + vendorTypeStr, + strings.Join(enabledTypes, ", ")) +} + +// getEnabledVendorTypes returns a list of enabled vendor type codes +func getEnabledVendorTypes(definitions []VendorTypeDefinition) []string { + var enabled []string + for _, def := range definitions { + if def.Spec.Enabled { + enabled = append(enabled, def.Spec.Code) + } + } + return enabled +} + +// GetVendorTypeDisplayName returns the display name for a vendor type code +func GetVendorTypeDisplayName(vendorType VendorType, definition *VendorTypeDefinition) string { + if definition == nil { + return string(vendorType) + } + + vendorTypeStr := string(vendorType) + if definition.Spec.Code == vendorTypeStr { + return definition.Spec.DisplayName + } + return vendorTypeStr +} + +// GetVendorTypeDisplayNameFromList returns the display name for a vendor type code from a list +func GetVendorTypeDisplayNameFromList(vendorType VendorType, definitions []VendorTypeDefinition) string { + vendorTypeStr := string(vendorType) + for _, def := range definitions { + if def.Spec.Code == vendorTypeStr { + return def.Spec.DisplayName + } + } + return vendorTypeStr +} + +// GetAvailableVendorTypes returns all available vendor type definitions +func GetAvailableVendorTypes(definitions []VendorTypeDefinition) []VendorTypeDefinition { + var available []VendorTypeDefinition + for _, def := range definitions { + if def.Spec.Enabled { + available = append(available, def) + } + } + return available +} + +// FindVendorTypeDefinition finds a vendor type definition by code +func FindVendorTypeDefinition(code string, definitions []VendorTypeDefinition) *VendorTypeDefinition { + for _, def := range definitions { + if def.Spec.Code == code { + return &def + } + } + return nil +} diff --git a/pkg/apis/vendors/v1alpha1/vendor_types.go b/pkg/apis/vendors/v1alpha1/vendor_types.go new file mode 100644 index 00000000..a8be497b --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/vendor_types.go @@ -0,0 +1,248 @@ +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VendorProfileType defines the type of vendor profile +// +kubebuilder:validation:Enum=person;business +type VendorProfileType string + +const ( + VendorProfileTypePerson VendorProfileType = "person" + VendorProfileTypeBusiness VendorProfileType = "business" +) + +// VendorStatusValue defines the current status of a vendor +// +kubebuilder:validation:Enum=pending;active;rejected;archived +type VendorStatusValue string + +const ( + VendorStatusPending VendorStatusValue = "pending" + VendorStatusActive VendorStatusValue = "active" + VendorStatusRejected VendorStatusValue = "rejected" + VendorStatusArchived VendorStatusValue = "archived" +) + +// VendorType defines the type of vendor +// This should reference a valid vendor type from VendorTypeDefinition +// +kubebuilder:validation:Pattern=^[a-z0-9-]+$ +type VendorType string + +// TaxIdType defines the type of tax identification +// +kubebuilder:validation:Enum=SSN;EIN;ITIN;UNSPECIFIED +type TaxIdType string + +const ( + TaxIdTypeSSN TaxIdType = "SSN" + TaxIdTypeEIN TaxIdType = "EIN" + TaxIdTypeITIN TaxIdType = "ITIN" + TaxIdTypeUnspecified TaxIdType = "UNSPECIFIED" +) + +// Address represents a physical address +type Address struct { + // Street address line 1 + // +kubebuilder:validation:Required + Street string `json:"street"` + + // Street address line 2 (optional) + // +optional + Street2 string `json:"street2,omitempty"` + + // City + // +kubebuilder:validation:Required + City string `json:"city"` + + // State or province + // +kubebuilder:validation:Required + State string `json:"state"` + + // Postal or ZIP code + // +kubebuilder:validation:Required + PostalCode string `json:"postalCode"` + + // Country + // +kubebuilder:validation:Required + Country string `json:"country"` +} + +// TaxIdReference references a tax ID stored in a Kubernetes Secret +type TaxIdReference struct { + // Name of the Secret containing the tax ID + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // Key within the Secret that contains the tax ID + // +kubebuilder:validation:Required + SecretKey string `json:"secretKey"` + + // Namespace of the Secret (if empty, uses the same namespace as the Vendor) + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// TaxInfo represents tax-related information +type TaxInfo struct { + // Type of tax identification + // +kubebuilder:validation:Required + TaxIdType TaxIdType `json:"taxIdType"` + + // Reference to the tax identification number stored in a Secret + // +kubebuilder:validation:Required + TaxIdRef TaxIdReference `json:"taxIdRef"` + + // Country for tax purposes + // +kubebuilder:validation:Required + Country string `json:"country"` + + // Tax document reference (e.g., W-9, W-8BEN) + // +kubebuilder:validation:Required + TaxDocument string `json:"taxDocument"` +} + +// VendorSpec defines the desired state of Vendor +// +k8s:protobuf=true +type VendorSpec struct { + // Profile type - person or business + // +kubebuilder:validation:Required + ProfileType VendorProfileType `json:"profileType"` + + // Legal name of the vendor (required) + // +kubebuilder:validation:Required + LegalName string `json:"legalName"` + + // Nickname or display name + // +optional + Nickname string `json:"nickname,omitempty"` + + // Billing address + // +kubebuilder:validation:Required + BillingAddress Address `json:"billingAddress"` + + // Mailing address (if different from billing) + // +optional + MailingAddress *Address `json:"mailingAddress,omitempty"` + + // Description of the vendor + // +optional + Description string `json:"description,omitempty"` + + // Website URL + // +optional + Website string `json:"website,omitempty"` + + // Business-specific fields (only applicable when profileType is business) + // +optional + VendorType VendorType `json:"vendorType,omitempty"` + + // Doing business as name + // +optional + CorporationDBA string `json:"corporationDBA,omitempty"` + + // Registration number (optional) + // +optional + RegistrationNumber string `json:"registrationNumber,omitempty"` + + // State of incorporation + // +optional + StateOfIncorporation string `json:"stateOfIncorporation,omitempty"` + + // Tax information + // +kubebuilder:validation:Required + TaxInfo TaxInfo `json:"taxInfo"` +} + +// VendorStatus defines the observed state of Vendor +// +k8s:protobuf=true +type VendorStatus struct { + // ObservedGeneration is the most recent generation observed for this Vendor by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represents the observations of a vendor's current state. + // Known condition types are: "Ready", "Validated", "Verified", "Active" + // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "Unknown", message: "Waiting for control plane to reconcile", lastTransitionTime: "1970-01-01T00:00:00Z"}} + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Current status of the vendor + // +optional + Status VendorStatusValue `json:"status,omitempty"` + + // Overall verification status + // +optional + VerificationStatus VerificationStatus `json:"verificationStatus,omitempty"` + + // Number of required verifications + // +optional + RequiredVerifications int32 `json:"requiredVerifications,omitempty"` + + // Number of completed verifications + // +optional + CompletedVerifications int32 `json:"completedVerifications,omitempty"` + + // Number of pending verifications + // +optional + PendingVerifications int32 `json:"pendingVerifications,omitempty"` + + // Number of rejected verifications + // +optional + RejectedVerifications int32 `json:"rejectedVerifications,omitempty"` + + // Number of expired verifications + // +optional + ExpiredVerifications int32 `json:"expiredVerifications,omitempty"` + + // Timestamp when vendor was last verified + // +optional + LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"` + + // Timestamp when vendor was activated + // +optional + ActivatedAt *metav1.Time `json:"activatedAt,omitempty"` + + // Timestamp when vendor was rejected + // +optional + RejectedAt *metav1.Time `json:"rejectedAt,omitempty"` + + // Reason for rejection (if applicable) + // +optional + RejectionReason string `json:"rejectionReason,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=vendors,scope=Cluster,categories=datum,singular=vendor +// +kubebuilder:printcolumn:name="Legal Name",type="string",JSONPath=".spec.legalName" +// +kubebuilder:printcolumn:name="Profile Type",type="string",JSONPath=".spec.profileType" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status" +// +kubebuilder:printcolumn:name="Verification",type="string",JSONPath=".status.verificationStatus" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" +// Vendor is the Schema for the Vendors API +// +kubebuilder:object:root=true +type Vendor struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec VendorSpec `json:"spec,omitempty"` + Status VendorStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:object:root=true +// VendorList contains a list of Vendor +type VendorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Vendor `json:"items"` +} diff --git a/pkg/apis/vendors/v1alpha1/vendortypedefinition_types.go b/pkg/apis/vendors/v1alpha1/vendortypedefinition_types.go new file mode 100644 index 00000000..016a9c2b --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/vendortypedefinition_types.go @@ -0,0 +1,108 @@ +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VendorTypeDefinitionSpec defines the desired state of VendorTypeDefinition +// +k8s:protobuf=true +type VendorTypeDefinitionSpec struct { + // The unique identifier for this vendor type (e.g., "llc", "s-corp", "partnership") + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=^[a-z0-9-]+$ + Code string `json:"code"` + + // Human-readable display name + // +kubebuilder:validation:Required + DisplayName string `json:"displayName"` + + // Optional description of this vendor type + // +optional + Description string `json:"description,omitempty"` + + // Whether this vendor type is currently available for selection + // +kubebuilder:validation:Required + // +kubebuilder:default=true + Enabled bool `json:"enabled"` + + // Category of vendor type (e.g., "business", "nonprofit", "international") + // +optional + Category string `json:"category,omitempty"` + + // Whether this type requires additional business-specific fields + // +kubebuilder:validation:Required + // +kubebuilder:default=false + RequiresBusinessFields bool `json:"requiresBusinessFields"` + + // Whether this type requires tax verification + // +kubebuilder:validation:Required + // +kubebuilder:default=true + RequiresTaxVerification bool `json:"requiresTaxVerification"` + + // Countries where this vendor type is valid (empty means all countries) + // +optional + ValidCountries []string `json:"validCountries,omitempty"` + + // Required tax document types for this vendor type + // +optional + RequiredTaxDocuments []string `json:"requiredTaxDocuments,omitempty"` +} + +// VendorTypeDefinitionStatus defines the observed state of VendorTypeDefinition +// +k8s:protobuf=true +type VendorTypeDefinitionStatus struct { + // ObservedGeneration is the most recent generation observed for this VendorTypeDefinition by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represents the observations of a vendor type definition's current state. + // Known condition types are: "Ready", "Valid" + // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "Unknown", message: "Waiting for control plane to reconcile", lastTransitionTime: "1970-01-01T00:00:00Z"}} + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Number of vendors currently using this type + // +optional + VendorCount int32 `json:"vendorCount,omitempty"` + + // Last time this type was used in a vendor + // +optional + LastUsed *metav1.Time `json:"lastUsed,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=vendortypedefinitions,scope=Cluster,categories=datum,singular=vendortypedefinition +// +kubebuilder:printcolumn:name="Code",type="string",JSONPath=".spec.code" +// +kubebuilder:printcolumn:name="Display Name",type="string",JSONPath=".spec.displayName" +// +kubebuilder:printcolumn:name="Enabled",type="boolean",JSONPath=".spec.enabled" +// +kubebuilder:printcolumn:name="Category",type="string",JSONPath=".spec.category" +// +kubebuilder:printcolumn:name="Vendor Count",type="integer",JSONPath=".status.vendorCount" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" +// VendorTypeDefinition is the Schema for the VendorTypeDefinitions API +// +kubebuilder:object:root=true +type VendorTypeDefinition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec VendorTypeDefinitionSpec `json:"spec,omitempty"` + Status VendorTypeDefinitionStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:object:root=true +// VendorTypeDefinitionList contains a list of VendorTypeDefinition +type VendorTypeDefinitionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VendorTypeDefinition `json:"items"` +} diff --git a/pkg/apis/vendors/v1alpha1/vendorverification_types.go b/pkg/apis/vendors/v1alpha1/vendorverification_types.go new file mode 100644 index 00000000..01d5c13f --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/vendorverification_types.go @@ -0,0 +1,193 @@ +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VerificationType defines the type of verification being performed +// +kubebuilder:validation:Enum=tax;business;identity;compliance;other +type VerificationType string + +const ( + VerificationTypeTax VerificationType = "tax" + VerificationTypeBusiness VerificationType = "business" + VerificationTypeIdentity VerificationType = "identity" + VerificationTypeCompliance VerificationType = "compliance" + VerificationTypeOther VerificationType = "other" +) + +// VerificationStatus defines the current status of a verification +// +kubebuilder:validation:Enum=pending;in-progress;approved;rejected;expired +type VerificationStatus string + +const ( + VerificationStatusPending VerificationStatus = "pending" + VerificationStatusInProgress VerificationStatus = "in-progress" + VerificationStatusApproved VerificationStatus = "approved" + VerificationStatusRejected VerificationStatus = "rejected" + VerificationStatusExpired VerificationStatus = "expired" +) + +// VendorReference references a Vendor resource +type VendorReference struct { + // Name of the Vendor resource + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Namespace of the Vendor resource (if empty, uses the same namespace as the VendorVerification) + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// VerifierReference references who performed the verification +type VerifierReference struct { + // Type of verifier (user, system, external-service, etc.) + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=user;system;external-service;admin + Type string `json:"type"` + + // Name of the verifier (username, service name, etc.) + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Additional metadata about the verifier + // +optional + Metadata map[string]string `json:"metadata,omitempty"` +} + +// VerificationDocument represents a document used in verification +type VerificationDocument struct { + // Type of document (W-9, W-8BEN, business-license, etc.) + // +kubebuilder:validation:Required + Type string `json:"type"` + + // Reference to the document (secret name, file path, etc.) + // +kubebuilder:validation:Required + Reference string `json:"reference"` + + // Document version or identifier + // +optional + Version string `json:"version,omitempty"` + + // Document expiration date + // +optional + ExpirationDate *metav1.Time `json:"expirationDate,omitempty"` + + // Whether the document is valid + // +kubebuilder:default=true + Valid bool `json:"valid"` +} + +// VendorVerificationSpec defines the desired state of VendorVerification +// +k8s:protobuf=true +type VendorVerificationSpec struct { + // Reference to the vendor being verified + // +kubebuilder:validation:Required + VendorRef VendorReference `json:"vendorRef"` + + // Type of verification being performed + // +kubebuilder:validation:Required + VerificationType VerificationType `json:"verificationType"` + + // Current status of the verification + // +kubebuilder:validation:Required + // +kubebuilder:default=pending + Status VerificationStatus `json:"status"` + + // Description of what is being verified + // +optional + Description string `json:"description,omitempty"` + + // Documents used in this verification + // +optional + Documents []VerificationDocument `json:"documents,omitempty"` + + // Reference to who is performing the verification + // +optional + VerifierRef *VerifierReference `json:"verifierRef,omitempty"` + + // Additional notes or comments about the verification + // +optional + Notes string `json:"notes,omitempty"` + + // Priority of this verification (1-10, higher is more urgent) + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=10 + // +kubebuilder:default=5 + Priority int32 `json:"priority"` + + // Expiration date for this verification + // +optional + ExpirationDate *metav1.Time `json:"expirationDate,omitempty"` + + // External system reference (if verification is done by external service) + // +optional + ExternalReference string `json:"externalReference,omitempty"` + + // Whether this verification is required for vendor activation + // +kubebuilder:default=true + Required bool `json:"required"` +} + +// VendorVerificationStatus defines the observed state of VendorVerification +// +k8s:protobuf=true +type VendorVerificationStatus struct { + // ObservedGeneration is the most recent generation observed for this VendorVerification by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represents the observations of a vendor verification's current state. + // Known condition types are: "Ready", "Valid", "Expired" + // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "Unknown", message: "Waiting for control plane to reconcile", lastTransitionTime: "1970-01-01T00:00:00Z"}} + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Timestamp when verification was last updated + // +optional + LastUpdatedAt *metav1.Time `json:"lastUpdatedAt,omitempty"` + + // Timestamp when verification was completed + // +optional + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + // Last error message if verification failed + // +optional + LastError string `json:"lastError,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=vendorverifications,scope=Cluster,categories=datum,singular=vendorverification +// +kubebuilder:printcolumn:name="Vendor",type="string",JSONPath=".spec.vendorRef.name" +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.verificationType" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".spec.status" +// +kubebuilder:printcolumn:name="Verifier",type="string",JSONPath=".spec.verifierRef.name" +// +kubebuilder:printcolumn:name="Required",type="boolean",JSONPath=".spec.required" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" +// VendorVerification is the Schema for the VendorVerifications API +// +kubebuilder:object:root=true +type VendorVerification struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec VendorVerificationSpec `json:"spec,omitempty"` + Status VendorVerificationStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:object:root=true +// VendorVerificationList contains a list of VendorVerification +type VendorVerificationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VendorVerification `json:"items"` +} diff --git a/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..30865b3a --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,458 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Address) DeepCopyInto(out *Address) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. +func (in *Address) DeepCopy() *Address { + if in == nil { + return nil + } + out := new(Address) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaxIdReference) DeepCopyInto(out *TaxIdReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaxIdReference. +func (in *TaxIdReference) DeepCopy() *TaxIdReference { + if in == nil { + return nil + } + out := new(TaxIdReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TaxInfo) DeepCopyInto(out *TaxInfo) { + *out = *in + out.TaxIdRef = in.TaxIdRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaxInfo. +func (in *TaxInfo) DeepCopy() *TaxInfo { + if in == nil { + return nil + } + out := new(TaxInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Vendor) DeepCopyInto(out *Vendor) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vendor. +func (in *Vendor) DeepCopy() *Vendor { + if in == nil { + return nil + } + out := new(Vendor) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Vendor) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorList) DeepCopyInto(out *VendorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Vendor, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorList. +func (in *VendorList) DeepCopy() *VendorList { + if in == nil { + return nil + } + out := new(VendorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VendorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorReference) DeepCopyInto(out *VendorReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorReference. +func (in *VendorReference) DeepCopy() *VendorReference { + if in == nil { + return nil + } + out := new(VendorReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorSpec) DeepCopyInto(out *VendorSpec) { + *out = *in + out.BillingAddress = in.BillingAddress + if in.MailingAddress != nil { + in, out := &in.MailingAddress, &out.MailingAddress + *out = new(Address) + **out = **in + } + out.TaxInfo = in.TaxInfo +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorSpec. +func (in *VendorSpec) DeepCopy() *VendorSpec { + if in == nil { + return nil + } + out := new(VendorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorStatus) DeepCopyInto(out *VendorStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LastVerifiedAt != nil { + in, out := &in.LastVerifiedAt, &out.LastVerifiedAt + *out = (*in).DeepCopy() + } + if in.ActivatedAt != nil { + in, out := &in.ActivatedAt, &out.ActivatedAt + *out = (*in).DeepCopy() + } + if in.RejectedAt != nil { + in, out := &in.RejectedAt, &out.RejectedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorStatus. +func (in *VendorStatus) DeepCopy() *VendorStatus { + if in == nil { + return nil + } + out := new(VendorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorTypeDefinition) DeepCopyInto(out *VendorTypeDefinition) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorTypeDefinition. +func (in *VendorTypeDefinition) DeepCopy() *VendorTypeDefinition { + if in == nil { + return nil + } + out := new(VendorTypeDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VendorTypeDefinition) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorTypeDefinitionList) DeepCopyInto(out *VendorTypeDefinitionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VendorTypeDefinition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorTypeDefinitionList. +func (in *VendorTypeDefinitionList) DeepCopy() *VendorTypeDefinitionList { + if in == nil { + return nil + } + out := new(VendorTypeDefinitionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VendorTypeDefinitionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorTypeDefinitionSpec) DeepCopyInto(out *VendorTypeDefinitionSpec) { + *out = *in + if in.ValidCountries != nil { + in, out := &in.ValidCountries, &out.ValidCountries + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequiredTaxDocuments != nil { + in, out := &in.RequiredTaxDocuments, &out.RequiredTaxDocuments + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorTypeDefinitionSpec. +func (in *VendorTypeDefinitionSpec) DeepCopy() *VendorTypeDefinitionSpec { + if in == nil { + return nil + } + out := new(VendorTypeDefinitionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorTypeDefinitionStatus) DeepCopyInto(out *VendorTypeDefinitionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LastUsed != nil { + in, out := &in.LastUsed, &out.LastUsed + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorTypeDefinitionStatus. +func (in *VendorTypeDefinitionStatus) DeepCopy() *VendorTypeDefinitionStatus { + if in == nil { + return nil + } + out := new(VendorTypeDefinitionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorVerification) DeepCopyInto(out *VendorVerification) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorVerification. +func (in *VendorVerification) DeepCopy() *VendorVerification { + if in == nil { + return nil + } + out := new(VendorVerification) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VendorVerification) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorVerificationList) DeepCopyInto(out *VendorVerificationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VendorVerification, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorVerificationList. +func (in *VendorVerificationList) DeepCopy() *VendorVerificationList { + if in == nil { + return nil + } + out := new(VendorVerificationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VendorVerificationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorVerificationSpec) DeepCopyInto(out *VendorVerificationSpec) { + *out = *in + out.VendorRef = in.VendorRef + if in.Documents != nil { + in, out := &in.Documents, &out.Documents + *out = make([]VerificationDocument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VerifierRef != nil { + in, out := &in.VerifierRef, &out.VerifierRef + *out = new(VerifierReference) + (*in).DeepCopyInto(*out) + } + if in.ExpirationDate != nil { + in, out := &in.ExpirationDate, &out.ExpirationDate + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorVerificationSpec. +func (in *VendorVerificationSpec) DeepCopy() *VendorVerificationSpec { + if in == nil { + return nil + } + out := new(VendorVerificationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VendorVerificationStatus) DeepCopyInto(out *VendorVerificationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LastUpdatedAt != nil { + in, out := &in.LastUpdatedAt, &out.LastUpdatedAt + *out = (*in).DeepCopy() + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VendorVerificationStatus. +func (in *VendorVerificationStatus) DeepCopy() *VendorVerificationStatus { + if in == nil { + return nil + } + out := new(VendorVerificationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VerificationDocument) DeepCopyInto(out *VerificationDocument) { + *out = *in + if in.ExpirationDate != nil { + in, out := &in.ExpirationDate, &out.ExpirationDate + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerificationDocument. +func (in *VerificationDocument) DeepCopy() *VerificationDocument { + if in == nil { + return nil + } + out := new(VerificationDocument) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VerifierReference) DeepCopyInto(out *VerifierReference) { + *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerifierReference. +func (in *VerifierReference) DeepCopy() *VerifierReference { + if in == nil { + return nil + } + out := new(VerifierReference) + in.DeepCopyInto(out) + return out +}