From aef0e1b27f0423092abd695c6544e0467fe73c8e Mon Sep 17 00:00:00 2001 From: Emiliano Jankowski Date: Mon, 15 Sep 2025 09:49:29 +0200 Subject: [PATCH 1/3] [Fixes #195] Adding vendors support --- .../bases/resourcemanager/kustomization.yaml | 2 + ...r.miloapis.com_corporationtypeconfigs.yaml | 187 ++++++++++++ .../resourcemanager.miloapis.com_vendors.yaml | 279 ++++++++++++++++++ .../corporationtypeconfig-example.yaml | 79 +++++ .../v1alpha1/vendor-example.yaml | 63 ++++ docs/api/dynamic-corporation-types.md | 189 ++++++++++++ .../vendor_validation_controller.go | 79 +++++ .../v1alpha1/corporationtypeconfig_types.go | 97 ++++++ pkg/apis/resourcemanager/v1alpha1/register.go | 4 + .../resourcemanager/v1alpha1/validation.go | 65 ++++ .../resourcemanager/v1alpha1/vendor_types.go | 201 +++++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 253 ++++++++++++++++ 12 files changed, 1498 insertions(+) create mode 100644 config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml create mode 100644 config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml create mode 100644 config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml create mode 100644 config/samples/resourcemanager/v1alpha1/vendor-example.yaml create mode 100644 docs/api/dynamic-corporation-types.md create mode 100644 internal/controllers/resourcemanager/vendor_validation_controller.go create mode 100644 pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go create mode 100644 pkg/apis/resourcemanager/v1alpha1/validation.go create mode 100644 pkg/apis/resourcemanager/v1alpha1/vendor_types.go diff --git a/config/crd/bases/resourcemanager/kustomization.yaml b/config/crd/bases/resourcemanager/kustomization.yaml index d67ee33a..be912933 100644 --- a/config/crd/bases/resourcemanager/kustomization.yaml +++ b/config/crd/bases/resourcemanager/kustomization.yaml @@ -2,3 +2,5 @@ resources: - resourcemanager.miloapis.com_organizations.yaml - resourcemanager.miloapis.com_projects.yaml - resourcemanager.miloapis.com_organizationmemberships.yaml +- resourcemanager.miloapis.com_vendors.yaml +- resourcemanager.miloapis.com_corporationtypeconfigs.yaml diff --git a/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml b/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml new file mode 100644 index 00000000..8d70f9ea --- /dev/null +++ b/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml @@ -0,0 +1,187 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: corporationtypeconfigs.resourcemanager.miloapis.com +spec: + group: resourcemanager.miloapis.com + names: + categories: + - datum + kind: CorporationTypeConfig + listKind: CorporationTypeConfigList + plural: corporationtypeconfigs + singular: corporationtypeconfig + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.active + name: Active + type: boolean + - jsonPath: .status.activeTypeCount + name: Type 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: CorporationTypeConfig is the Schema for the CorporationTypeConfigs + 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: CorporationTypeConfigSpec defines the desired state of CorporationTypeConfig + properties: + active: + default: true + description: Whether this configuration is active + type: boolean + corporationTypes: + description: Available corporation types that can be selected for + vendors + items: + description: CorporationTypeDefinition defines a single corporation + type option + properties: + code: + description: The unique identifier for this corporation type + pattern: ^[a-z0-9-]+$ + type: string + description: + description: Optional description of this corporation type + type: string + displayName: + description: Human-readable display name + type: string + enabled: + default: true + description: Whether this corporation type is currently available + for selection + type: boolean + sortOrder: + default: 100 + description: Sort order for display purposes (lower numbers + appear first) + format: int32 + type: integer + required: + - code + - displayName + - enabled + - sortOrder + type: object + minItems: 1 + type: array + required: + - active + - corporationTypes + type: object + status: + description: CorporationTypeConfigStatus defines the observed state of + CorporationTypeConfig + properties: + activeTypeCount: + description: Number of active corporation types + 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 corporation type config's current state. + Known condition types are: "Ready" + 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 + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this CorporationTypeConfig by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml b/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml new file mode 100644 index 00000000..42bdc5d9 --- /dev/null +++ b/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml @@ -0,0 +1,279 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vendors.resourcemanager.miloapis.com +spec: + group: resourcemanager.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: .spec.status + name: Status + 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 + corporationType: + description: Business-specific fields (only applicable when profileType + is business) + pattern: ^[a-z0-9-]+$ + 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 + status: + default: pending + description: Current status of the vendor + enum: + - pending + - active + - rejected + - archived + 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 + taxId: + description: Tax identification number + type: string + taxIdType: + description: Type of tax identification + enum: + - SSN + - EIN + - ITIN + - UNSPECIFIED + type: string + taxVerified: + default: false + description: Whether tax information has been verified + type: boolean + verificationTimestamp: + description: Timestamp of tax verification + format: date-time + type: string + required: + - country + - taxDocument + - taxId + - taxIdType + - taxVerified + type: object + website: + description: Website URL + type: string + required: + - billingAddress + - legalName + - profileType + - status + - taxInfo + type: object + status: + description: VendorStatus defines the observed state of Vendor + 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's current state. + Known condition types are: "Ready" + 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 + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this Vendor by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml b/config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml new file mode 100644 index 00000000..99b2b5ab --- /dev/null +++ b/config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml @@ -0,0 +1,79 @@ +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: CorporationTypeConfig +metadata: + name: default-corporation-types + annotations: + kubernetes.io/display-name: "Default Corporation Types" +spec: + active: true + corporationTypes: + - 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 + sortOrder: 10 + - 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 + sortOrder: 20 + - 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 + sortOrder: 30 + - 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 + sortOrder: 40 + - 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 + sortOrder: 50 + - 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 + sortOrder: 60 + - code: "b-corp" + displayName: "Benefit Corporation (B-Corp)" + description: "A type of for-profit corporate entity that includes positive impact on society, workers, the community and the environment in addition to profit as legally defined goals" + enabled: true + sortOrder: 70 + - code: "other" + displayName: "Other" + description: "Other business structure not listed above" + enabled: true + sortOrder: 100 +--- +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: CorporationTypeConfig +metadata: + name: international-corporation-types + annotations: + kubernetes.io/display-name: "International Corporation Types" +spec: + active: false + corporationTypes: + - code: "ltd" + displayName: "Limited Company (Ltd.)" + description: "A private limited company, common in the UK and other Commonwealth countries" + enabled: true + sortOrder: 10 + - code: "gmbh" + displayName: "Gesellschaft mit beschränkter Haftung (GmbH)" + description: "A type of legal entity in Germany, Austria, and Switzerland" + enabled: true + sortOrder: 20 + - code: "sarl" + displayName: "Société à Responsabilité Limitée (SARL)" + description: "A type of business entity in France and other French-speaking countries" + enabled: true + sortOrder: 30 + - code: "bv" + displayName: "Besloten Vennootschap (B.V.)" + description: "A private limited liability company in the Netherlands" + enabled: true + sortOrder: 40 diff --git a/config/samples/resourcemanager/v1alpha1/vendor-example.yaml b/config/samples/resourcemanager/v1alpha1/vendor-example.yaml new file mode 100644 index 00000000..a6c13e1b --- /dev/null +++ b/config/samples/resourcemanager/v1alpha1/vendor-example.yaml @@ -0,0 +1,63 @@ +apiVersion: resourcemanager.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" + status: active + corporationType: 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 + taxId: "12-3456789" + country: "United States" + taxDocument: "W-9" + taxVerified: true + verificationTimestamp: "2024-01-15T10:30:00Z" +--- +apiVersion: resourcemanager.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" + status: pending + billingAddress: + street: "789 Home St" + city: "San Francisco" + state: "CA" + postalCode: "94102" + country: "United States" + taxInfo: + taxIdType: SSN + taxId: "123-45-6789" + country: "United States" + taxDocument: "W-9" + taxVerified: false diff --git a/docs/api/dynamic-corporation-types.md b/docs/api/dynamic-corporation-types.md new file mode 100644 index 00000000..9d0756f5 --- /dev/null +++ b/docs/api/dynamic-corporation-types.md @@ -0,0 +1,189 @@ +# Dynamic Corporation Types + +This document explains how to manage corporation types dynamically in the Milo system using the `CorporationTypeConfig` CRD. + +## Overview + +Instead of hardcoded corporation types, the system now supports dynamic corporation types that can be managed by staff users through Kubernetes resources. This allows for: + +- Adding new corporation types without code changes +- Enabling/disabling corporation types +- Customizing display names and descriptions +- Organizing types with sort orders + +## Architecture + +### CorporationTypeConfig CRD + +The `CorporationTypeConfig` CRD allows staff users to define available corporation types: + +```yaml +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: CorporationTypeConfig +metadata: + name: default-corporation-types +spec: + active: true + corporationTypes: + - code: "llc" + displayName: "Limited Liability Company (LLC)" + description: "A business structure that combines..." + enabled: true + sortOrder: 10 +``` + +### Vendor CRD Updates + +The `Vendor` CRD now uses a string field for `corporationType` that references the codes defined in `CorporationTypeConfig`: + +```yaml +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: acme-corp +spec: + profileType: business + legalName: "ACME Corporation LLC" + corporationType: "llc" # References code from CorporationTypeConfig + # ... other fields +``` + +## Usage + +### 1. Create Corporation Type Configuration + +Create a `CorporationTypeConfig` resource with your desired corporation types: + +```yaml +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: CorporationTypeConfig +metadata: + name: my-corporation-types +spec: + active: true + corporationTypes: + - code: "llc" + displayName: "LLC" + description: "Limited Liability Company" + enabled: true + sortOrder: 10 + - code: "s-corp" + displayName: "S Corporation" + description: "S Corporation" + enabled: true + sortOrder: 20 + - code: "custom-type" + displayName: "Custom Business Type" + description: "Our custom business structure" + enabled: true + sortOrder: 30 +``` + +### 2. Create Vendors with Dynamic Types + +When creating vendors, use the codes defined in your `CorporationTypeConfig`: + +```yaml +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Vendor +metadata: + name: example-vendor +spec: + profileType: business + legalName: "Example Business" + corporationType: "llc" # Must match a code from CorporationTypeConfig + # ... other fields +``` + +### 3. Managing Corporation Types + +#### Adding New Types + +To add a new corporation type, update your `CorporationTypeConfig`: + +```yaml +spec: + corporationTypes: + # ... existing types + - code: "new-type" + displayName: "New Business Type" + description: "A new type of business structure" + enabled: true + sortOrder: 40 +``` + +#### Disabling Types + +To disable a corporation type without removing it: + +```yaml +spec: + corporationTypes: + - code: "old-type" + displayName: "Old Business Type" + enabled: false # Disable this type + sortOrder: 50 +``` + +#### Reordering Types + +Use the `sortOrder` field to control display order (lower numbers appear first): + +```yaml +spec: + corporationTypes: + - code: "priority-type" + displayName: "Priority Type" + enabled: true + sortOrder: 5 # Will appear first + - code: "other-type" + displayName: "Other Type" + enabled: true + sortOrder: 100 # Will appear last +``` + +## Validation + +The system validates that: + +1. Only one `CorporationTypeConfig` can be active at a time +2. Corporation type codes must be unique within a config +3. Corporation type codes must match the pattern `^[a-z0-9-]+$` +4. Vendor `corporationType` values must reference valid, enabled codes + +## API Functions + +The system provides helper functions for validation and display: + +```go +// Validate a corporation type against a config +err := ValidateCorporationType(vendor.Spec.CorporationType, config) + +// Get display name for a corporation type +displayName := GetCorporationTypeDisplayName(vendor.Spec.CorporationType, config) + +// Get all available corporation types +types := GetAvailableCorporationTypes(config) +``` + +## Migration from Hardcoded Types + +If you have existing vendors with hardcoded corporation types, you'll need to: + +1. Create a `CorporationTypeConfig` with the old hardcoded values +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 corporation type changes in a development environment first + +## Examples + +See the sample files: +- `config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml` +- `config/samples/resourcemanager/v1alpha1/vendor-example.yaml` diff --git a/internal/controllers/resourcemanager/vendor_validation_controller.go b/internal/controllers/resourcemanager/vendor_validation_controller.go new file mode 100644 index 00000000..762a3a14 --- /dev/null +++ b/internal/controllers/resourcemanager/vendor_validation_controller.go @@ -0,0 +1,79 @@ +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" + + resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" +) + +// VendorValidationReconciler reconciles Vendor objects to validate corporation types +type VendorValidationReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=vendors,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=vendors/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=corporationtypeconfigs,verbs=get;list;watch + +// Reconcile validates vendor corporation types against active CorporationTypeConfig +func (r *VendorValidationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // Fetch the Vendor + var vendor resourcemanagerv1alpha1.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 corporationType is not set + if vendor.Spec.CorporationType == "" { + return ctrl.Result{}, nil + } + + // Find the active CorporationTypeConfig + var configList resourcemanagerv1alpha1.CorporationTypeConfigList + if err := r.List(ctx, &configList); err != nil { + logger.Error(err, "unable to list CorporationTypeConfigs") + return ctrl.Result{}, err + } + + var activeConfig *resourcemanagerv1alpha1.CorporationTypeConfig + for _, config := range configList.Items { + if config.Spec.Active { + activeConfig = &config + break + } + } + + if activeConfig == nil { + logger.Info("no active CorporationTypeConfig found, skipping validation") + return ctrl.Result{}, nil + } + + // Validate the corporation type + if err := resourcemanagerv1alpha1.ValidateCorporationType(vendor.Spec.CorporationType, activeConfig); err != nil { + logger.Error(err, "invalid corporation type", "corporationType", vendor.Spec.CorporationType) + + // 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 corporation type %q: %w", vendor.Spec.CorporationType, err) + } + + logger.Info("vendor corporation type validated successfully", "corporationType", vendor.Spec.CorporationType) + 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(&resourcemanagerv1alpha1.Vendor{}). + Complete(r) +} diff --git a/pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go b/pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go new file mode 100644 index 00000000..a99cacb9 --- /dev/null +++ b/pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go @@ -0,0 +1,97 @@ +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CorporationTypeConfigSpec defines the desired state of CorporationTypeConfig +// +k8s:protobuf=true +type CorporationTypeConfigSpec struct { + // Available corporation types that can be selected for vendors + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + CorporationTypes []CorporationTypeDefinition `json:"corporationTypes"` + + // Whether this configuration is active + // +kubebuilder:validation:Required + // +kubebuilder:default=true + Active bool `json:"active"` +} + +// CorporationTypeDefinition defines a single corporation type option +type CorporationTypeDefinition struct { + // The unique identifier for this corporation type + // +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 corporation type + // +optional + Description string `json:"description,omitempty"` + + // Whether this corporation type is currently available for selection + // +kubebuilder:validation:Required + // +kubebuilder:default=true + Enabled bool `json:"enabled"` + + // Sort order for display purposes (lower numbers appear first) + // +kubebuilder:validation:Required + // +kubebuilder:default=100 + SortOrder int32 `json:"sortOrder"` +} + +// CorporationTypeConfigStatus defines the observed state of CorporationTypeConfig +// +k8s:protobuf=true +type CorporationTypeConfigStatus struct { + // ObservedGeneration is the most recent generation observed for this CorporationTypeConfig by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represents the observations of a corporation type config's current state. + // Known condition types are: "Ready" + // +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 active corporation types + // +optional + ActiveTypeCount int32 `json:"activeTypeCount,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=corporationtypeconfigs,scope=Cluster,categories=datum,singular=corporationtypeconfig +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".spec.active" +// +kubebuilder:printcolumn:name="Type Count",type="integer",JSONPath=".status.activeTypeCount" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" +// CorporationTypeConfig is the Schema for the CorporationTypeConfigs API +// +kubebuilder:object:root=true +type CorporationTypeConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec CorporationTypeConfigSpec `json:"spec,omitempty"` + Status CorporationTypeConfigStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:protobuf=true + +// +kubebuilder:object:root=true +// CorporationTypeConfigList contains a list of CorporationTypeConfig +type CorporationTypeConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CorporationTypeConfig `json:"items"` +} diff --git a/pkg/apis/resourcemanager/v1alpha1/register.go b/pkg/apis/resourcemanager/v1alpha1/register.go index ad7bbdc1..8087f097 100644 --- a/pkg/apis/resourcemanager/v1alpha1/register.go +++ b/pkg/apis/resourcemanager/v1alpha1/register.go @@ -34,6 +34,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OrganizationList{}, &OrganizationMembership{}, &OrganizationMembershipList{}, + &Vendor{}, + &VendorList{}, + &CorporationTypeConfig{}, + &CorporationTypeConfigList{}, ) metav1.AddToGroupVersion(scheme, GroupVersion) return nil diff --git a/pkg/apis/resourcemanager/v1alpha1/validation.go b/pkg/apis/resourcemanager/v1alpha1/validation.go new file mode 100644 index 00000000..ff19ba7a --- /dev/null +++ b/pkg/apis/resourcemanager/v1alpha1/validation.go @@ -0,0 +1,65 @@ +package v1alpha1 + +import ( + "fmt" + "strings" +) + +// ValidateCorporationType validates that a corporation type is valid according to the provided config +func ValidateCorporationType(corpType CorporationType, config *CorporationTypeConfig) error { + if config == nil || !config.Spec.Active { + return fmt.Errorf("no active corporation type configuration found") + } + + corpTypeStr := string(corpType) + if corpTypeStr == "" { + return nil // Optional field + } + + for _, typeDef := range config.Spec.CorporationTypes { + if typeDef.Code == corpTypeStr { + if !typeDef.Enabled { + return fmt.Errorf("corporation type %q is disabled", corpTypeStr) + } + return nil + } + } + + return fmt.Errorf("invalid corporation type %q, must be one of: %s", + corpTypeStr, + strings.Join(getEnabledCorporationTypes(config), ", ")) +} + +// getEnabledCorporationTypes returns a list of enabled corporation type codes +func getEnabledCorporationTypes(config *CorporationTypeConfig) []string { + var enabled []string + for _, typeDef := range config.Spec.CorporationTypes { + if typeDef.Enabled { + enabled = append(enabled, typeDef.Code) + } + } + return enabled +} + +// GetCorporationTypeDisplayName returns the display name for a corporation type code +func GetCorporationTypeDisplayName(corpType CorporationType, config *CorporationTypeConfig) string { + if config == nil { + return string(corpType) + } + + corpTypeStr := string(corpType) + for _, typeDef := range config.Spec.CorporationTypes { + if typeDef.Code == corpTypeStr { + return typeDef.DisplayName + } + } + return corpTypeStr +} + +// GetAvailableCorporationTypes returns all available corporation types from the config +func GetAvailableCorporationTypes(config *CorporationTypeConfig) []CorporationTypeDefinition { + if config == nil || !config.Spec.Active { + return nil + } + return config.Spec.CorporationTypes +} diff --git a/pkg/apis/resourcemanager/v1alpha1/vendor_types.go b/pkg/apis/resourcemanager/v1alpha1/vendor_types.go new file mode 100644 index 00000000..85d7e77d --- /dev/null +++ b/pkg/apis/resourcemanager/v1alpha1/vendor_types.go @@ -0,0 +1,201 @@ +// +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" +) + +// CorporationType defines the type of corporation +// This should reference a valid corporation type from CorporationTypeConfig +// +kubebuilder:validation:Pattern=^[a-z0-9-]+$ +type CorporationType 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"` +} + +// TaxInfo represents tax-related information +type TaxInfo struct { + // Type of tax identification + // +kubebuilder:validation:Required + TaxIdType TaxIdType `json:"taxIdType"` + + // Tax identification number + // +kubebuilder:validation:Required + TaxId string `json:"taxId"` + + // 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"` + + // Whether tax information has been verified + // +kubebuilder:default=false + TaxVerified bool `json:"taxVerified"` + + // Timestamp of tax verification + // +optional + VerificationTimestamp *metav1.Time `json:"verificationTimestamp,omitempty"` +} + +// 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"` + + // Current status of the vendor + // +kubebuilder:validation:Required + // +kubebuilder:default=pending + Status VendorStatusValue `json:"status"` + + // Business-specific fields (only applicable when profileType is business) + // +optional + CorporationType CorporationType `json:"corporationType,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" + // +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"` +} + +// +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=".spec.status" +// +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/resourcemanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go index 4bfe8695..1a7a4bca 100644 --- a/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go @@ -9,6 +9,137 @@ import ( "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 *CorporationTypeConfig) DeepCopyInto(out *CorporationTypeConfig) { + *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 CorporationTypeConfig. +func (in *CorporationTypeConfig) DeepCopy() *CorporationTypeConfig { + if in == nil { + return nil + } + out := new(CorporationTypeConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CorporationTypeConfig) 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 *CorporationTypeConfigList) DeepCopyInto(out *CorporationTypeConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CorporationTypeConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigList. +func (in *CorporationTypeConfigList) DeepCopy() *CorporationTypeConfigList { + if in == nil { + return nil + } + out := new(CorporationTypeConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CorporationTypeConfigList) 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 *CorporationTypeConfigSpec) DeepCopyInto(out *CorporationTypeConfigSpec) { + *out = *in + if in.CorporationTypes != nil { + in, out := &in.CorporationTypes, &out.CorporationTypes + *out = make([]CorporationTypeDefinition, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigSpec. +func (in *CorporationTypeConfigSpec) DeepCopy() *CorporationTypeConfigSpec { + if in == nil { + return nil + } + out := new(CorporationTypeConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CorporationTypeConfigStatus) DeepCopyInto(out *CorporationTypeConfigStatus) { + *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]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigStatus. +func (in *CorporationTypeConfigStatus) DeepCopy() *CorporationTypeConfigStatus { + if in == nil { + return nil + } + out := new(CorporationTypeConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CorporationTypeDefinition) DeepCopyInto(out *CorporationTypeDefinition) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeDefinition. +func (in *CorporationTypeDefinition) DeepCopy() *CorporationTypeDefinition { + if in == nil { + return nil + } + out := new(CorporationTypeDefinition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MemberReference) DeepCopyInto(out *MemberReference) { *out = *in @@ -376,3 +507,125 @@ func (in *ProjectStatus) DeepCopy() *ProjectStatus { 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 + if in.VerificationTimestamp != nil { + in, out := &in.VerificationTimestamp, &out.VerificationTimestamp + *out = (*in).DeepCopy() + } +} + +// 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 *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 + } + in.TaxInfo.DeepCopyInto(&out.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]) + } + } +} + +// 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 +} From d2df109d5b43f9a074361e6258b7043bc5a88438 Mon Sep 17 00:00:00 2001 From: Emiliano Jankowski Date: Wed, 24 Sep 2025 12:54:16 +0200 Subject: [PATCH 2/3] handling comments --- .../controller-manager/controllermanager.go | 2 + .../bases/resourcemanager/kustomization.yaml | 2 - config/crd/bases/vendors/kustomization.yaml | 4 + .../vendors.miloapis.com_vendors.yaml} | 115 +++-- ...s.miloapis.com_vendortypedefinitions.yaml} | 133 ++--- ...dors.miloapis.com_vendorverifications.yaml | 281 +++++++++++ .../core-control-plane/kustomization.yaml | 1 + .../corporationtypeconfig-example.yaml | 79 --- .../v1alpha1/vendor-example.yaml | 63 --- .../v1alpha1/tax-id-secrets-example.yaml | 59 +++ .../vendors/v1alpha1/vendor-example.yaml | 124 +++++ .../vendortypedefinition-example.yaml | 169 +++++++ .../v1alpha1/vendorverification-example.yaml | 184 +++++++ docs/api/dynamic-corporation-types.md | 234 +++++---- docs/api/infrastructure.md | 192 +++----- docs/api/resourcemanager.md | 64 +-- docs/api/tax-id-secrets.md | 329 +++++++++++++ docs/api/vendor-status-conditions.md | 303 ++++++++++++ docs/api/vendor-type-definitions.md | 197 ++++++++ docs/api/vendor-verification.md | 340 +++++++++++++ docs/api/vendors-api-group.md | 173 +++++++ .../vendor_validation_controller.go | 57 +-- internal/controllers/vendors/status_utils.go | 286 +++++++++++ internal/controllers/vendors/taxid_utils.go | 165 +++++++ .../controllers/vendors/verification_utils.go | 257 ++++++++++ .../v1alpha1/corporationtypeconfig_types.go | 97 ---- pkg/apis/resourcemanager/v1alpha1/register.go | 4 - .../resourcemanager/v1alpha1/validation.go | 65 --- .../v1alpha1/zz_generated.deepcopy.go | 253 ---------- pkg/apis/vendors/scheme.go | 11 + pkg/apis/vendors/v1alpha1/doc.go | 5 + pkg/apis/vendors/v1alpha1/register.go | 44 ++ pkg/apis/vendors/v1alpha1/validation.go | 106 ++++ .../v1alpha1/vendor_types.go | 89 +++- .../v1alpha1/vendortypedefinition_types.go | 108 +++++ .../v1alpha1/vendorverification_types.go | 197 ++++++++ .../vendors/v1alpha1/zz_generated.deepcopy.go | 458 ++++++++++++++++++ 37 files changed, 4262 insertions(+), 988 deletions(-) create mode 100644 config/crd/bases/vendors/kustomization.yaml rename config/crd/bases/{resourcemanager/resourcemanager.miloapis.com_vendors.yaml => vendors/vendors.miloapis.com_vendors.yaml} (77%) rename config/crd/bases/{resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml => vendors/vendors.miloapis.com_vendortypedefinitions.yaml} (64%) create mode 100644 config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml delete mode 100644 config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml delete mode 100644 config/samples/resourcemanager/v1alpha1/vendor-example.yaml create mode 100644 config/samples/vendors/v1alpha1/tax-id-secrets-example.yaml create mode 100644 config/samples/vendors/v1alpha1/vendor-example.yaml create mode 100644 config/samples/vendors/v1alpha1/vendortypedefinition-example.yaml create mode 100644 config/samples/vendors/v1alpha1/vendorverification-example.yaml create mode 100644 docs/api/tax-id-secrets.md create mode 100644 docs/api/vendor-status-conditions.md create mode 100644 docs/api/vendor-type-definitions.md create mode 100644 docs/api/vendor-verification.md create mode 100644 docs/api/vendors-api-group.md create mode 100644 internal/controllers/vendors/status_utils.go create mode 100644 internal/controllers/vendors/taxid_utils.go create mode 100644 internal/controllers/vendors/verification_utils.go delete mode 100644 pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go delete mode 100644 pkg/apis/resourcemanager/v1alpha1/validation.go create mode 100644 pkg/apis/vendors/scheme.go create mode 100644 pkg/apis/vendors/v1alpha1/doc.go create mode 100644 pkg/apis/vendors/v1alpha1/register.go create mode 100644 pkg/apis/vendors/v1alpha1/validation.go rename pkg/apis/{resourcemanager => vendors}/v1alpha1/vendor_types.go (72%) create mode 100644 pkg/apis/vendors/v1alpha1/vendortypedefinition_types.go create mode 100644 pkg/apis/vendors/v1alpha1/vendorverification_types.go create mode 100644 pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 1ba66283..ed8b252c 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/crd/bases/resourcemanager/kustomization.yaml b/config/crd/bases/resourcemanager/kustomization.yaml index be912933..d67ee33a 100644 --- a/config/crd/bases/resourcemanager/kustomization.yaml +++ b/config/crd/bases/resourcemanager/kustomization.yaml @@ -2,5 +2,3 @@ resources: - resourcemanager.miloapis.com_organizations.yaml - resourcemanager.miloapis.com_projects.yaml - resourcemanager.miloapis.com_organizationmemberships.yaml -- resourcemanager.miloapis.com_vendors.yaml -- resourcemanager.miloapis.com_corporationtypeconfigs.yaml 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/resourcemanager/resourcemanager.miloapis.com_vendors.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml similarity index 77% rename from config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml rename to config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml index 42bdc5d9..30da2e9a 100644 --- a/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_vendors.yaml +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendors.yaml @@ -4,9 +4,9 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 - name: vendors.resourcemanager.miloapis.com + name: vendors.vendors.miloapis.com spec: - group: resourcemanager.miloapis.com + group: vendors.miloapis.com names: categories: - datum @@ -23,9 +23,12 @@ spec: - jsonPath: .spec.profileType name: Profile Type type: string - - jsonPath: .spec.status + - jsonPath: .status.status name: Status type: string + - jsonPath: .status.verificationStatus + name: Verification + type: string - jsonPath: .status.conditions[?(@.type=='Ready')].status name: Ready type: string @@ -88,11 +91,6 @@ spec: corporationDBA: description: Doing business as name type: string - corporationType: - description: Business-specific fields (only applicable when profileType - is business) - pattern: ^[a-z0-9-]+$ - type: string description: description: Description of the vendor type: string @@ -142,15 +140,6 @@ spec: stateOfIncorporation: description: State of incorporation type: string - status: - default: pending - description: Current status of the vendor - enum: - - pending - - active - - rejected - - archived - type: string taxInfo: description: Tax information properties: @@ -160,9 +149,24 @@ spec: taxDocument: description: Tax document reference (e.g., W-9, W-8BEN) type: string - taxId: - description: Tax identification number - 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: @@ -171,21 +175,17 @@ spec: - ITIN - UNSPECIFIED type: string - taxVerified: - default: false - description: Whether tax information has been verified - type: boolean - verificationTimestamp: - description: Timestamp of tax verification - format: date-time - type: string required: - country - taxDocument - - taxId + - taxIdRef - taxIdType - - taxVerified 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 @@ -193,12 +193,19 @@ spec: - billingAddress - legalName - profileType - - status - 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" @@ -208,7 +215,7 @@ spec: type: Ready description: |- Conditions represents the observations of a vendor's current state. - Known condition types are: "Ready" + Known condition types are: "Ready", "Validated", "Verified", "Active" items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -264,11 +271,55 @@ spec: - 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 diff --git a/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml similarity index 64% rename from config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml rename to config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml index 8d70f9ea..6c02cc12 100644 --- a/config/crd/bases/resourcemanager/resourcemanager.miloapis.com_corporationtypeconfigs.yaml +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendortypedefinitions.yaml @@ -4,24 +4,33 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 - name: corporationtypeconfigs.resourcemanager.miloapis.com + name: vendortypedefinitions.vendors.miloapis.com spec: - group: resourcemanager.miloapis.com + group: vendors.miloapis.com names: categories: - datum - kind: CorporationTypeConfig - listKind: CorporationTypeConfigList - plural: corporationtypeconfigs - singular: corporationtypeconfig + kind: VendorTypeDefinition + listKind: VendorTypeDefinitionList + plural: vendortypedefinitions + singular: vendortypedefinition scope: Cluster versions: - additionalPrinterColumns: - - jsonPath: .spec.active - name: Active + - jsonPath: .spec.code + name: Code + type: string + - jsonPath: .spec.displayName + name: Display Name + type: string + - jsonPath: .spec.enabled + name: Enabled type: boolean - - jsonPath: .status.activeTypeCount - name: Type Count + - jsonPath: .spec.category + name: Category + type: string + - jsonPath: .status.vendorCount + name: Vendor Count type: integer - jsonPath: .status.conditions[?(@.type=='Ready')].status name: Ready @@ -32,7 +41,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: CorporationTypeConfig is the Schema for the CorporationTypeConfigs + description: VendorTypeDefinition is the Schema for the VendorTypeDefinitions API properties: apiVersion: @@ -53,60 +62,58 @@ spec: metadata: type: object spec: - description: CorporationTypeConfigSpec defines the desired state of CorporationTypeConfig + description: VendorTypeDefinitionSpec defines the desired state of VendorTypeDefinition properties: - active: + 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 configuration is active + description: Whether this vendor type is currently available for selection type: boolean - corporationTypes: - description: Available corporation types that can be selected for - vendors + requiredTaxDocuments: + description: Required tax document types for this vendor type items: - description: CorporationTypeDefinition defines a single corporation - type option - properties: - code: - description: The unique identifier for this corporation type - pattern: ^[a-z0-9-]+$ - type: string - description: - description: Optional description of this corporation type - type: string - displayName: - description: Human-readable display name - type: string - enabled: - default: true - description: Whether this corporation type is currently available - for selection - type: boolean - sortOrder: - default: 100 - description: Sort order for display purposes (lower numbers - appear first) - format: int32 - type: integer - required: - - code - - displayName - - enabled - - sortOrder - type: object - minItems: 1 + 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: - - active - - corporationTypes + - code + - displayName + - enabled + - requiresBusinessFields + - requiresTaxVerification type: object status: - description: CorporationTypeConfigStatus defines the observed state of - CorporationTypeConfig + description: VendorTypeDefinitionStatus defines the observed state of + VendorTypeDefinition properties: - activeTypeCount: - description: Number of active corporation types - format: int32 - type: integer conditions: default: - lastTransitionTime: "1970-01-01T00:00:00Z" @@ -115,8 +122,8 @@ spec: status: Unknown type: Ready description: |- - Conditions represents the observations of a corporation type config's current state. - Known condition types are: "Ready" + 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. @@ -172,11 +179,19 @@ spec: - 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 CorporationTypeConfig by the controller. + 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 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..1796a7c9 --- /dev/null +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml @@ -0,0 +1,281 @@ +--- +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: + attemptCount: + description: Number of verification attempts + format: int32 + type: integer + 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/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml b/config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml deleted file mode 100644 index 99b2b5ab..00000000 --- a/config/samples/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: resourcemanager.miloapis.com/v1alpha1 -kind: CorporationTypeConfig -metadata: - name: default-corporation-types - annotations: - kubernetes.io/display-name: "Default Corporation Types" -spec: - active: true - corporationTypes: - - 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 - sortOrder: 10 - - 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 - sortOrder: 20 - - 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 - sortOrder: 30 - - 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 - sortOrder: 40 - - 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 - sortOrder: 50 - - 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 - sortOrder: 60 - - code: "b-corp" - displayName: "Benefit Corporation (B-Corp)" - description: "A type of for-profit corporate entity that includes positive impact on society, workers, the community and the environment in addition to profit as legally defined goals" - enabled: true - sortOrder: 70 - - code: "other" - displayName: "Other" - description: "Other business structure not listed above" - enabled: true - sortOrder: 100 ---- -apiVersion: resourcemanager.miloapis.com/v1alpha1 -kind: CorporationTypeConfig -metadata: - name: international-corporation-types - annotations: - kubernetes.io/display-name: "International Corporation Types" -spec: - active: false - corporationTypes: - - code: "ltd" - displayName: "Limited Company (Ltd.)" - description: "A private limited company, common in the UK and other Commonwealth countries" - enabled: true - sortOrder: 10 - - code: "gmbh" - displayName: "Gesellschaft mit beschränkter Haftung (GmbH)" - description: "A type of legal entity in Germany, Austria, and Switzerland" - enabled: true - sortOrder: 20 - - code: "sarl" - displayName: "Société à Responsabilité Limitée (SARL)" - description: "A type of business entity in France and other French-speaking countries" - enabled: true - sortOrder: 30 - - code: "bv" - displayName: "Besloten Vennootschap (B.V.)" - description: "A private limited liability company in the Netherlands" - enabled: true - sortOrder: 40 diff --git a/config/samples/resourcemanager/v1alpha1/vendor-example.yaml b/config/samples/resourcemanager/v1alpha1/vendor-example.yaml deleted file mode 100644 index a6c13e1b..00000000 --- a/config/samples/resourcemanager/v1alpha1/vendor-example.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: resourcemanager.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" - status: active - corporationType: 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 - taxId: "12-3456789" - country: "United States" - taxDocument: "W-9" - taxVerified: true - verificationTimestamp: "2024-01-15T10:30:00Z" ---- -apiVersion: resourcemanager.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" - status: pending - billingAddress: - street: "789 Home St" - city: "San Francisco" - state: "CA" - postalCode: "94102" - country: "United States" - taxInfo: - taxIdType: SSN - taxId: "123-45-6789" - country: "United States" - taxDocument: "W-9" - taxVerified: false 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..d4330b31 --- /dev/null +++ b/config/samples/vendors/v1alpha1/vendorverification-example.yaml @@ -0,0 +1,184 @@ +# 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" + attemptCount: 1 +--- +# 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" + attemptCount: 1 +--- +# 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" + attemptCount: 2 + 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" + attemptCount: 0 +--- +# 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" + attemptCount: 3 + 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 index 9d0756f5..be2f9be5 100644 --- a/docs/api/dynamic-corporation-types.md +++ b/docs/api/dynamic-corporation-types.md @@ -1,128 +1,153 @@ -# Dynamic Corporation Types +# Dynamic Vendor Types -This document explains how to manage corporation types dynamically in the Milo system using the `CorporationTypeConfig` CRD. +This document explains how to manage vendor types dynamically in the Milo system using individual `VendorTypeDefinition` resources. ## Overview -Instead of hardcoded corporation types, the system now supports dynamic corporation types that can be managed by staff users through Kubernetes resources. This allows for: +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 corporation types without code changes -- Enabling/disabling corporation types +- 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 -### CorporationTypeConfig CRD +### VendorTypeDefinition CRD -The `CorporationTypeConfig` CRD allows staff users to define available corporation types: +Each vendor type is now a separate `VendorTypeDefinition` resource with its own spec and status: ```yaml -apiVersion: resourcemanager.miloapis.com/v1alpha1 -kind: CorporationTypeConfig +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition metadata: - name: default-corporation-types + name: llc spec: - active: true - corporationTypes: - - code: "llc" - displayName: "Limited Liability Company (LLC)" - description: "A business structure that combines..." - enabled: true - sortOrder: 10 + 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 `corporationType` that references the codes defined in `CorporationTypeConfig`: +The `Vendor` CRD now uses a string field for `vendorType` that references the codes defined in `VendorTypeDefinition` resources: ```yaml -apiVersion: resourcemanager.miloapis.com/v1alpha1 +apiVersion: vendors.miloapis.com/v1alpha1 kind: Vendor metadata: name: acme-corp spec: profileType: business legalName: "ACME Corporation LLC" - corporationType: "llc" # References code from CorporationTypeConfig + vendorType: "llc" # References code from VendorTypeDefinition # ... other fields ``` ## Usage -### 1. Create Corporation Type Configuration +### 1. Create Individual Vendor Type Definitions -Create a `CorporationTypeConfig` resource with your desired corporation types: +Create separate `VendorTypeDefinition` resources for each vendor type: ```yaml -apiVersion: resourcemanager.miloapis.com/v1alpha1 -kind: CorporationTypeConfig +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition metadata: - name: my-corporation-types + name: llc spec: - active: true - corporationTypes: - - code: "llc" - displayName: "LLC" - description: "Limited Liability Company" - enabled: true - sortOrder: 10 - - code: "s-corp" - displayName: "S Corporation" - description: "S Corporation" - enabled: true - sortOrder: 20 - - code: "custom-type" - displayName: "Custom Business Type" - description: "Our custom business structure" - enabled: true - sortOrder: 30 + 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 `CorporationTypeConfig`: +When creating vendors, use the codes defined in your `VendorTypeDefinition` resources: ```yaml -apiVersion: resourcemanager.miloapis.com/v1alpha1 +apiVersion: vendors.miloapis.com/v1alpha1 kind: Vendor metadata: name: example-vendor spec: profileType: business legalName: "Example Business" - corporationType: "llc" # Must match a code from CorporationTypeConfig + vendorType: "llc" # Must match a code from VendorTypeDefinition # ... other fields ``` -### 3. Managing Corporation Types +### 3. Managing Vendor Types #### Adding New Types -To add a new corporation type, update your `CorporationTypeConfig`: +To add a new vendor type, create a new `VendorTypeDefinition` resource: ```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: new-type spec: - corporationTypes: - # ... existing types - - code: "new-type" - displayName: "New Business Type" - description: "A new type of business structure" - enabled: true - sortOrder: 40 + 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 corporation type without removing it: +To disable a vendor type, update its `enabled` field: ```yaml +apiVersion: vendors.miloapis.com/v1alpha1 +kind: VendorTypeDefinition +metadata: + name: old-type spec: - corporationTypes: - - code: "old-type" - displayName: "Old Business Type" - enabled: false # Disable this type - sortOrder: 50 + code: "old-type" + displayName: "Old Business Type" + enabled: false # Disable this type + sortOrder: 50 + # ... other fields ``` #### Reordering Types @@ -130,47 +155,57 @@ spec: 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: - corporationTypes: - - code: "priority-type" - displayName: "Priority Type" - enabled: true - sortOrder: 5 # Will appear first - - code: "other-type" - displayName: "Other Type" - enabled: true - sortOrder: 100 # Will appear last + code: "priority-type" + displayName: "Priority Type" + enabled: true + sortOrder: 5 # Will appear first + # ... other fields ``` ## Validation The system validates that: -1. Only one `CorporationTypeConfig` can be active at a time -2. Corporation type codes must be unique within a config -3. Corporation type codes must match the pattern `^[a-z0-9-]+$` -4. Vendor `corporationType` values must reference valid, enabled codes +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 corporation type against a config -err := ValidateCorporationType(vendor.Spec.CorporationType, config) +// 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 display name for a corporation type -displayName := GetCorporationTypeDisplayName(vendor.Spec.CorporationType, config) +// Get all available vendor types +availableTypes := GetAvailableVendorTypes(definitions) -// Get all available corporation types -types := GetAvailableCorporationTypes(config) +// Find a specific vendor type definition +definition := FindVendorTypeDefinition("llc", definitions) ``` ## Migration from Hardcoded Types -If you have existing vendors with hardcoded corporation types, you'll need to: +If you have existing vendors with hardcoded vendor types, you'll need to: -1. Create a `CorporationTypeConfig` with the old hardcoded values +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 @@ -180,10 +215,41 @@ If you have existing vendors with hardcoded corporation types, you'll need to: 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 corporation type changes in a development environment first +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/resourcemanager/v1alpha1/corporationtypeconfig-example.yaml` -- `config/samples/resourcemanager/v1alpha1/vendor-example.yaml` +- `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/tax-id-secrets.md b/docs/api/tax-id-secrets.md new file mode 100644 index 00000000..0e95b544 --- /dev/null +++ b/docs/api/tax-id-secrets.md @@ -0,0 +1,329 @@ +# Tax ID Secret Management + +This document explains how tax ID numbers are securely stored and managed using Kubernetes Secrets in the Milo vendor system. + +## Overview + +Tax ID numbers are sensitive information that should never be stored in plain text in CRDs. Instead, they are stored in Kubernetes Secrets and referenced by the Vendor resource through a `TaxIdReference`. + +## Architecture + +### TaxIdReference + +The `TaxIdReference` type points to a Kubernetes Secret containing the tax ID: + +```yaml +taxIdRef: + secretName: "acme-corp-tax-id" # Name of the Secret + secretKey: "tax-id" # Key within the Secret + namespace: "default" # Optional: Secret namespace +``` + +### Secret Structure + +Tax IDs are stored in Kubernetes Secrets with the following structure: + +```yaml +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: + tax-id: MTItMzQ1Njc4OQ== # Base64 encoded tax ID +``` + +## Security Benefits + +### 1. **Encryption at Rest** +- Secrets are encrypted at rest by Kubernetes +- No plain text storage in etcd +- Automatic encryption key rotation + +### 2. **Access Control** +- RBAC controls who can access secrets +- Fine-grained permissions per secret +- Audit logging for secret access + +### 3. **Separation of Concerns** +- Sensitive data separated from business logic +- CRDs contain only references, not actual data +- Easier to manage and audit + +### 4. **Compliance** +- Meets data protection requirements +- Audit trail for sensitive data access +- Proper data handling practices + +## Usage Examples + +### Creating a Tax ID Secret + +```bash +# Create a secret with tax ID +kubectl create secret generic acme-corp-tax-id \ + --from-literal=tax-id="12-3456789" \ + --namespace=default + +# Add labels for identification +kubectl label secret acme-corp-tax-id \ + vendor.miloapis.com/vendor=acme-corp \ + vendor.miloapis.com/type=tax-id +``` + +### Referencing in Vendor Resource + +```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" + namespace: "default" + country: "United States" + taxDocument: "W-9" + taxVerified: true +``` + +### Multiple Tax IDs + +For vendors with multiple tax IDs (e.g., international operations): + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: complex-vendor-tax-ids + namespace: default +type: Opaque +data: + ein: MTItMzQ1Njc4OQ== # US EIN + vat: R0IxMjM0NTY3ODk= # EU VAT + business-number: Q0ExMjM0NTY3ODk= # Canadian Business Number +``` + +```yaml +# Reference specific tax ID +taxIdRef: + secretName: "complex-vendor-tax-ids" + secretKey: "ein" # or "vat", "business-number" + namespace: "default" +``` + +## API Functions + +### Retrieving Tax ID + +```go +// Get tax ID from secret +taxId, err := vendorsv1alpha1.GetTaxIdFromSecret(ctx, client, vendor, taxIdRef) +if err != nil { + return fmt.Errorf("failed to get tax ID: %w", err) +} +``` + +### Validating Secret Reference + +```go +// Validate that secret exists and contains the key +err := vendorsv1alpha1.ValidateTaxIdSecret(ctx, client, vendor, taxIdRef) +if err != nil { + return fmt.Errorf("invalid tax ID secret: %w", err) +} +``` + +### Creating/Updating Secrets + +```go +// Create new secret +err := vendorsv1alpha1.CreateTaxIdSecret(ctx, client, vendor, taxIdRef, "12-3456789") + +// Update existing secret +err := vendorsv1alpha1.UpdateTaxIdSecret(ctx, client, vendor, taxIdRef, "12-3456789") + +// Delete secret +err := vendorsv1alpha1.DeleteTaxIdSecret(ctx, client, vendor, taxIdRef) +``` + +## Best Practices + +### 1. **Secret Naming Convention** +Use descriptive names that include the vendor name: +```bash +# Good +acme-corp-tax-id +john-doe-ssn +international-corp-vat + +# Avoid +tax-secret-1 +secret-abc +``` + +### 2. **Key Naming Convention** +Use consistent key names within secrets: +```yaml +data: + tax-id: MTItMzQ1Njc4OQ== # For EIN + ssn: MTIzLTQ1LTY3ODk= # For SSN + vat-number: R0IxMjM0NTY3ODk= # For VAT +``` + +### 3. **Namespace Strategy** +- Use the same namespace as the vendor when possible +- Specify namespace explicitly for cross-namespace references +- Consider using a dedicated namespace for sensitive data + +### 4. **Labels and Annotations** +Add proper labels for identification and management: +```yaml +metadata: + labels: + vendor.miloapis.com/vendor: acme-corp + vendor.miloapis.com/type: tax-id + vendor.miloapis.com/country: us + annotations: + vendor.miloapis.com/tax-id-type: ein + vendor.miloapis.com/created-by: admin +``` + +### 5. **Owner References** +Set owner references to ensure cleanup: +```yaml +ownerReferences: + - apiVersion: vendors.miloapis.com/v1alpha1 + kind: Vendor + name: acme-corp + uid: 12345678-1234-1234-1234-123456789abc + controller: true +``` + +## Migration from Plain Text + +If you have existing vendors with plain text tax IDs: + +1. **Create secrets** for each vendor's tax ID +2. **Update vendor resources** to use `TaxIdRef` instead of `TaxId` +3. **Verify secrets** are accessible and contain correct data +4. **Test validation** to ensure everything works + +### Migration Script Example + +```bash +#!/bin/bash +# Extract tax IDs and create secrets +kubectl get vendors -o json | jq -r '.items[] | select(.spec.taxInfo.taxId) | "\(.metadata.name) \(.spec.taxInfo.taxId)"' | while read vendor_name tax_id; do + # Create secret + kubectl create secret generic "${vendor_name}-tax-id" \ + --from-literal=tax-id="$tax_id" \ + --namespace=default + + # Add labels + kubectl label secret "${vendor_name}-tax-id" \ + vendor.miloapis.com/vendor="$vendor_name" \ + vendor.miloapis.com/type=tax-id +done +``` + +## Security Considerations + +### 1. **RBAC Configuration** +Ensure proper RBAC for secret access: +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vendor-secret-reader +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + resourceNames: ["*-tax-id"] # Restrict to tax ID secrets +``` + +### 2. **Network Policies** +Consider network policies to restrict secret access: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: restrict-secret-access +spec: + podSelector: {} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: vendor-controllers +``` + +### 3. **Audit Logging** +Enable audit logging for secret access: +```yaml +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: +- level: Metadata + resources: + - group: "" + resources: ["secrets"] +``` + +## Troubleshooting + +### Common Issues + +1. **Secret Not Found** + ``` + Error: failed to get secret default/acme-corp-tax-id: secrets "acme-corp-tax-id" not found + ``` + - Check secret name and namespace + - Verify secret exists: `kubectl get secret acme-corp-tax-id` + +2. **Key Not Found** + ``` + Error: key tax-id not found in secret default/acme-corp-tax-id + ``` + - Check secret key name + - Verify key exists: `kubectl get secret acme-corp-tax-id -o yaml` + +3. **Permission Denied** + ``` + Error: secrets "acme-corp-tax-id" is forbidden: User cannot get resource "secrets" + ``` + - Check RBAC permissions + - Verify user has access to secrets + +### Debugging Commands + +```bash +# Check secret exists +kubectl get secret acme-corp-tax-id + +# View secret contents (base64 encoded) +kubectl get secret acme-corp-tax-id -o yaml + +# Decode secret value +kubectl get secret acme-corp-tax-id -o jsonpath='{.data.tax-id}' | base64 -d + +# Check secret labels +kubectl get secret acme-corp-tax-id --show-labels + +# Test secret access +kubectl auth can-i get secret acme-corp-tax-id +``` + +This approach ensures that sensitive tax ID information is properly secured while maintaining the flexibility and functionality of the vendor management system. 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..fb0f3d62 --- /dev/null +++ b/docs/api/vendor-verification.md @@ -0,0 +1,340 @@ +# 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" + attemptCount: 1 +``` + +## 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..0c312865 --- /dev/null +++ b/docs/api/vendors-api-group.md @@ -0,0 +1,173 @@ +# 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 +``` + +## Next Steps + +1. **Deploy the new CRDs** to your cluster +2. **Update any existing vendor resources** to use the new API group +3. **Update client applications** to reference `vendors.miloapis.com/v1alpha1` +4. **Consider adding vendor-specific controllers** for business logic +5. **Add vendor-specific webhooks** for validation and admission control + +## Backward Compatibility + +This is a breaking change that requires updating existing vendor resources to use the new API group. The resource structure remains the same, only the API group and version change. + +### Migration Script Example + +```bash +# Update existing vendor resources +kubectl get vendors -o yaml | sed 's/resourcemanager.miloapis.com/vendors.miloapis.com/g' | kubectl apply -f - +``` diff --git a/internal/controllers/resourcemanager/vendor_validation_controller.go b/internal/controllers/resourcemanager/vendor_validation_controller.go index 762a3a14..12954ee6 100644 --- a/internal/controllers/resourcemanager/vendor_validation_controller.go +++ b/internal/controllers/resourcemanager/vendor_validation_controller.go @@ -9,7 +9,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" - resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" + 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 @@ -18,62 +19,56 @@ type VendorValidationReconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=vendors,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=vendors/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=resourcemanager.miloapis.com,resources=corporationtypeconfigs,verbs=get;list;watch +//+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 corporation types against active CorporationTypeConfig +// 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 resourcemanagerv1alpha1.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 corporationType is not set - if vendor.Spec.CorporationType == "" { + // Skip validation if vendorType is not set + if vendor.Spec.VendorType == "" { return ctrl.Result{}, nil } - // Find the active CorporationTypeConfig - var configList resourcemanagerv1alpha1.CorporationTypeConfigList - if err := r.List(ctx, &configList); err != nil { - logger.Error(err, "unable to list CorporationTypeConfigs") + // 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 } - var activeConfig *resourcemanagerv1alpha1.CorporationTypeConfig - for _, config := range configList.Items { - if config.Spec.Active { - activeConfig = &config - break - } - } - - if activeConfig == nil { - logger.Info("no active CorporationTypeConfig found, skipping validation") - return ctrl.Result{}, nil - } - - // Validate the corporation type - if err := resourcemanagerv1alpha1.ValidateCorporationType(vendor.Spec.CorporationType, activeConfig); err != nil { - logger.Error(err, "invalid corporation type", "corporationType", vendor.Spec.CorporationType) + // 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 corporation type %q: %w", vendor.Spec.CorporationType, err) + 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 corporation type validated successfully", "corporationType", vendor.Spec.CorporationType) + 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(&resourcemanagerv1alpha1.Vendor{}). + 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..cdc4c055 --- /dev/null +++ b/internal/controllers/vendors/verification_utils.go @@ -0,0 +1,257 @@ +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 + } + + // Increment attempt count + verification.Status.AttemptCount++ + + 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/resourcemanager/v1alpha1/corporationtypeconfig_types.go b/pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go deleted file mode 100644 index a99cacb9..00000000 --- a/pkg/apis/resourcemanager/v1alpha1/corporationtypeconfig_types.go +++ /dev/null @@ -1,97 +0,0 @@ -// +kubebuilder:object:generate=true -package v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// CorporationTypeConfigSpec defines the desired state of CorporationTypeConfig -// +k8s:protobuf=true -type CorporationTypeConfigSpec struct { - // Available corporation types that can be selected for vendors - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 - CorporationTypes []CorporationTypeDefinition `json:"corporationTypes"` - - // Whether this configuration is active - // +kubebuilder:validation:Required - // +kubebuilder:default=true - Active bool `json:"active"` -} - -// CorporationTypeDefinition defines a single corporation type option -type CorporationTypeDefinition struct { - // The unique identifier for this corporation type - // +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 corporation type - // +optional - Description string `json:"description,omitempty"` - - // Whether this corporation type is currently available for selection - // +kubebuilder:validation:Required - // +kubebuilder:default=true - Enabled bool `json:"enabled"` - - // Sort order for display purposes (lower numbers appear first) - // +kubebuilder:validation:Required - // +kubebuilder:default=100 - SortOrder int32 `json:"sortOrder"` -} - -// CorporationTypeConfigStatus defines the observed state of CorporationTypeConfig -// +k8s:protobuf=true -type CorporationTypeConfigStatus struct { - // ObservedGeneration is the most recent generation observed for this CorporationTypeConfig by the controller. - // +optional - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - - // Conditions represents the observations of a corporation type config's current state. - // Known condition types are: "Ready" - // +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 active corporation types - // +optional - ActiveTypeCount int32 `json:"activeTypeCount,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +k8s:protobuf=true - -// +kubebuilder:subresource:status -// +kubebuilder:resource:path=corporationtypeconfigs,scope=Cluster,categories=datum,singular=corporationtypeconfig -// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".spec.active" -// +kubebuilder:printcolumn:name="Type Count",type="integer",JSONPath=".status.activeTypeCount" -// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" -// CorporationTypeConfig is the Schema for the CorporationTypeConfigs API -// +kubebuilder:object:root=true -type CorporationTypeConfig struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - // +kubebuilder:validation:Required - Spec CorporationTypeConfigSpec `json:"spec,omitempty"` - Status CorporationTypeConfigStatus `json:"status,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +k8s:protobuf=true - -// +kubebuilder:object:root=true -// CorporationTypeConfigList contains a list of CorporationTypeConfig -type CorporationTypeConfigList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []CorporationTypeConfig `json:"items"` -} diff --git a/pkg/apis/resourcemanager/v1alpha1/register.go b/pkg/apis/resourcemanager/v1alpha1/register.go index 8087f097..ad7bbdc1 100644 --- a/pkg/apis/resourcemanager/v1alpha1/register.go +++ b/pkg/apis/resourcemanager/v1alpha1/register.go @@ -34,10 +34,6 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OrganizationList{}, &OrganizationMembership{}, &OrganizationMembershipList{}, - &Vendor{}, - &VendorList{}, - &CorporationTypeConfig{}, - &CorporationTypeConfigList{}, ) metav1.AddToGroupVersion(scheme, GroupVersion) return nil diff --git a/pkg/apis/resourcemanager/v1alpha1/validation.go b/pkg/apis/resourcemanager/v1alpha1/validation.go deleted file mode 100644 index ff19ba7a..00000000 --- a/pkg/apis/resourcemanager/v1alpha1/validation.go +++ /dev/null @@ -1,65 +0,0 @@ -package v1alpha1 - -import ( - "fmt" - "strings" -) - -// ValidateCorporationType validates that a corporation type is valid according to the provided config -func ValidateCorporationType(corpType CorporationType, config *CorporationTypeConfig) error { - if config == nil || !config.Spec.Active { - return fmt.Errorf("no active corporation type configuration found") - } - - corpTypeStr := string(corpType) - if corpTypeStr == "" { - return nil // Optional field - } - - for _, typeDef := range config.Spec.CorporationTypes { - if typeDef.Code == corpTypeStr { - if !typeDef.Enabled { - return fmt.Errorf("corporation type %q is disabled", corpTypeStr) - } - return nil - } - } - - return fmt.Errorf("invalid corporation type %q, must be one of: %s", - corpTypeStr, - strings.Join(getEnabledCorporationTypes(config), ", ")) -} - -// getEnabledCorporationTypes returns a list of enabled corporation type codes -func getEnabledCorporationTypes(config *CorporationTypeConfig) []string { - var enabled []string - for _, typeDef := range config.Spec.CorporationTypes { - if typeDef.Enabled { - enabled = append(enabled, typeDef.Code) - } - } - return enabled -} - -// GetCorporationTypeDisplayName returns the display name for a corporation type code -func GetCorporationTypeDisplayName(corpType CorporationType, config *CorporationTypeConfig) string { - if config == nil { - return string(corpType) - } - - corpTypeStr := string(corpType) - for _, typeDef := range config.Spec.CorporationTypes { - if typeDef.Code == corpTypeStr { - return typeDef.DisplayName - } - } - return corpTypeStr -} - -// GetAvailableCorporationTypes returns all available corporation types from the config -func GetAvailableCorporationTypes(config *CorporationTypeConfig) []CorporationTypeDefinition { - if config == nil || !config.Spec.Active { - return nil - } - return config.Spec.CorporationTypes -} diff --git a/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go index 1a7a4bca..4bfe8695 100644 --- a/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/resourcemanager/v1alpha1/zz_generated.deepcopy.go @@ -9,137 +9,6 @@ import ( "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 *CorporationTypeConfig) DeepCopyInto(out *CorporationTypeConfig) { - *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 CorporationTypeConfig. -func (in *CorporationTypeConfig) DeepCopy() *CorporationTypeConfig { - if in == nil { - return nil - } - out := new(CorporationTypeConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *CorporationTypeConfig) 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 *CorporationTypeConfigList) DeepCopyInto(out *CorporationTypeConfigList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]CorporationTypeConfig, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigList. -func (in *CorporationTypeConfigList) DeepCopy() *CorporationTypeConfigList { - if in == nil { - return nil - } - out := new(CorporationTypeConfigList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *CorporationTypeConfigList) 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 *CorporationTypeConfigSpec) DeepCopyInto(out *CorporationTypeConfigSpec) { - *out = *in - if in.CorporationTypes != nil { - in, out := &in.CorporationTypes, &out.CorporationTypes - *out = make([]CorporationTypeDefinition, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigSpec. -func (in *CorporationTypeConfigSpec) DeepCopy() *CorporationTypeConfigSpec { - if in == nil { - return nil - } - out := new(CorporationTypeConfigSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CorporationTypeConfigStatus) DeepCopyInto(out *CorporationTypeConfigStatus) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeConfigStatus. -func (in *CorporationTypeConfigStatus) DeepCopy() *CorporationTypeConfigStatus { - if in == nil { - return nil - } - out := new(CorporationTypeConfigStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CorporationTypeDefinition) DeepCopyInto(out *CorporationTypeDefinition) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorporationTypeDefinition. -func (in *CorporationTypeDefinition) DeepCopy() *CorporationTypeDefinition { - if in == nil { - return nil - } - out := new(CorporationTypeDefinition) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MemberReference) DeepCopyInto(out *MemberReference) { *out = *in @@ -507,125 +376,3 @@ func (in *ProjectStatus) DeepCopy() *ProjectStatus { 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 - if in.VerificationTimestamp != nil { - in, out := &in.VerificationTimestamp, &out.VerificationTimestamp - *out = (*in).DeepCopy() - } -} - -// 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 *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 - } - in.TaxInfo.DeepCopyInto(&out.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]) - } - } -} - -// 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 -} 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/resourcemanager/v1alpha1/vendor_types.go b/pkg/apis/vendors/v1alpha1/vendor_types.go similarity index 72% rename from pkg/apis/resourcemanager/v1alpha1/vendor_types.go rename to pkg/apis/vendors/v1alpha1/vendor_types.go index 85d7e77d..a8be497b 100644 --- a/pkg/apis/resourcemanager/v1alpha1/vendor_types.go +++ b/pkg/apis/vendors/v1alpha1/vendor_types.go @@ -25,10 +25,10 @@ const ( VendorStatusArchived VendorStatusValue = "archived" ) -// CorporationType defines the type of corporation -// This should reference a valid corporation type from CorporationTypeConfig +// VendorType defines the type of vendor +// This should reference a valid vendor type from VendorTypeDefinition // +kubebuilder:validation:Pattern=^[a-z0-9-]+$ -type CorporationType string +type VendorType string // TaxIdType defines the type of tax identification // +kubebuilder:validation:Enum=SSN;EIN;ITIN;UNSPECIFIED @@ -68,15 +68,30 @@ type Address struct { 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"` - // Tax identification number + // Reference to the tax identification number stored in a Secret // +kubebuilder:validation:Required - TaxId string `json:"taxId"` + TaxIdRef TaxIdReference `json:"taxIdRef"` // Country for tax purposes // +kubebuilder:validation:Required @@ -85,14 +100,6 @@ type TaxInfo struct { // Tax document reference (e.g., W-9, W-8BEN) // +kubebuilder:validation:Required TaxDocument string `json:"taxDocument"` - - // Whether tax information has been verified - // +kubebuilder:default=false - TaxVerified bool `json:"taxVerified"` - - // Timestamp of tax verification - // +optional - VerificationTimestamp *metav1.Time `json:"verificationTimestamp,omitempty"` } // VendorSpec defines the desired state of Vendor @@ -126,14 +133,9 @@ type VendorSpec struct { // +optional Website string `json:"website,omitempty"` - // Current status of the vendor - // +kubebuilder:validation:Required - // +kubebuilder:default=pending - Status VendorStatusValue `json:"status"` - // Business-specific fields (only applicable when profileType is business) // +optional - CorporationType CorporationType `json:"corporationType,omitempty"` + VendorType VendorType `json:"vendorType,omitempty"` // Doing business as name // +optional @@ -160,12 +162,56 @@ type VendorStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` // Conditions represents the observations of a vendor's current state. - // Known condition types are: "Ready" + // 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 @@ -175,7 +221,8 @@ type VendorStatus struct { // +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=".spec.status" +// +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 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..25674b63 --- /dev/null +++ b/pkg/apis/vendors/v1alpha1/vendorverification_types.go @@ -0,0 +1,197 @@ +// +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"` + + // Whether this verification is required for vendor activation + // +kubebuilder:default=true + Required bool `json:"required"` + + // 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"` +} + +// 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 completed + // +optional + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + // Timestamp when verification was last updated + // +optional + LastUpdatedAt *metav1.Time `json:"lastUpdatedAt,omitempty"` + + // Number of verification attempts + // +optional + AttemptCount int32 `json:"attemptCount,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..4576bd57 --- /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.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } + if in.LastUpdatedAt != nil { + in, out := &in.LastUpdatedAt, &out.LastUpdatedAt + *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 +} From 6e3a5a8c2d85e459b96e17479609096f68b2604c Mon Sep 17 00:00:00 2001 From: Emiliano Jankowski Date: Sun, 5 Oct 2025 22:56:29 +0200 Subject: [PATCH 3/3] fixing comments --- .../core-control-plane/rbac/role.yaml | 29 ++ ...dors.miloapis.com_vendorverifications.yaml | 4 - .../v1alpha1/vendorverification-example.yaml | 5 - docs/api/tax-id-secrets.md | 329 ------------------ docs/api/vendor-verification.md | 1 - docs/api/vendors-api-group.md | 19 - docs/guides/vendors/tax-id-management.md | 196 +++++++++++ .../controllers/vendors/verification_utils.go | 3 - .../v1alpha1/vendorverification_types.go | 16 +- .../vendors/v1alpha1/zz_generated.deepcopy.go | 8 +- 10 files changed, 235 insertions(+), 375 deletions(-) delete mode 100644 docs/api/tax-id-secrets.md create mode 100644 docs/guides/vendors/tax-id-management.md 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/vendors.miloapis.com_vendorverifications.yaml b/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml index 1796a7c9..f0394ed3 100644 --- a/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml +++ b/config/crd/bases/vendors/vendors.miloapis.com_vendorverifications.yaml @@ -186,10 +186,6 @@ spec: status: description: VendorVerificationStatus defines the observed state of VendorVerification properties: - attemptCount: - description: Number of verification attempts - format: int32 - type: integer completedAt: description: Timestamp when verification was completed format: date-time diff --git a/config/samples/vendors/v1alpha1/vendorverification-example.yaml b/config/samples/vendors/v1alpha1/vendorverification-example.yaml index d4330b31..6b5f3176 100644 --- a/config/samples/vendors/v1alpha1/vendorverification-example.yaml +++ b/config/samples/vendors/v1alpha1/vendorverification-example.yaml @@ -34,7 +34,6 @@ spec: status: completedAt: "2024-01-15T14:30:00Z" lastUpdatedAt: "2024-01-15T14:30:00Z" - attemptCount: 1 --- # Business verification for ACME Corporation apiVersion: vendors.miloapis.com/v1alpha1 @@ -72,7 +71,6 @@ spec: status: completedAt: "2024-01-16T09:15:00Z" lastUpdatedAt: "2024-01-16T09:15:00Z" - attemptCount: 1 --- # Identity verification for John Doe Consulting apiVersion: vendors.miloapis.com/v1alpha1 @@ -108,7 +106,6 @@ spec: required: true status: lastUpdatedAt: "2024-01-17T11:20:00Z" - attemptCount: 2 lastError: "Address verification document not provided" --- # Compliance verification for international vendor @@ -147,7 +144,6 @@ spec: externalReference: "COMP-2024-001234" status: lastUpdatedAt: "2024-01-18T08:45:00Z" - attemptCount: 0 --- # Rejected verification example apiVersion: vendors.miloapis.com/v1alpha1 @@ -180,5 +176,4 @@ spec: status: completedAt: "2024-01-19T16:30:00Z" lastUpdatedAt: "2024-01-19T16:30:00Z" - attemptCount: 3 lastError: "Invalid tax ID format and missing required fields" diff --git a/docs/api/tax-id-secrets.md b/docs/api/tax-id-secrets.md deleted file mode 100644 index 0e95b544..00000000 --- a/docs/api/tax-id-secrets.md +++ /dev/null @@ -1,329 +0,0 @@ -# Tax ID Secret Management - -This document explains how tax ID numbers are securely stored and managed using Kubernetes Secrets in the Milo vendor system. - -## Overview - -Tax ID numbers are sensitive information that should never be stored in plain text in CRDs. Instead, they are stored in Kubernetes Secrets and referenced by the Vendor resource through a `TaxIdReference`. - -## Architecture - -### TaxIdReference - -The `TaxIdReference` type points to a Kubernetes Secret containing the tax ID: - -```yaml -taxIdRef: - secretName: "acme-corp-tax-id" # Name of the Secret - secretKey: "tax-id" # Key within the Secret - namespace: "default" # Optional: Secret namespace -``` - -### Secret Structure - -Tax IDs are stored in Kubernetes Secrets with the following structure: - -```yaml -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: - tax-id: MTItMzQ1Njc4OQ== # Base64 encoded tax ID -``` - -## Security Benefits - -### 1. **Encryption at Rest** -- Secrets are encrypted at rest by Kubernetes -- No plain text storage in etcd -- Automatic encryption key rotation - -### 2. **Access Control** -- RBAC controls who can access secrets -- Fine-grained permissions per secret -- Audit logging for secret access - -### 3. **Separation of Concerns** -- Sensitive data separated from business logic -- CRDs contain only references, not actual data -- Easier to manage and audit - -### 4. **Compliance** -- Meets data protection requirements -- Audit trail for sensitive data access -- Proper data handling practices - -## Usage Examples - -### Creating a Tax ID Secret - -```bash -# Create a secret with tax ID -kubectl create secret generic acme-corp-tax-id \ - --from-literal=tax-id="12-3456789" \ - --namespace=default - -# Add labels for identification -kubectl label secret acme-corp-tax-id \ - vendor.miloapis.com/vendor=acme-corp \ - vendor.miloapis.com/type=tax-id -``` - -### Referencing in Vendor Resource - -```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" - namespace: "default" - country: "United States" - taxDocument: "W-9" - taxVerified: true -``` - -### Multiple Tax IDs - -For vendors with multiple tax IDs (e.g., international operations): - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: complex-vendor-tax-ids - namespace: default -type: Opaque -data: - ein: MTItMzQ1Njc4OQ== # US EIN - vat: R0IxMjM0NTY3ODk= # EU VAT - business-number: Q0ExMjM0NTY3ODk= # Canadian Business Number -``` - -```yaml -# Reference specific tax ID -taxIdRef: - secretName: "complex-vendor-tax-ids" - secretKey: "ein" # or "vat", "business-number" - namespace: "default" -``` - -## API Functions - -### Retrieving Tax ID - -```go -// Get tax ID from secret -taxId, err := vendorsv1alpha1.GetTaxIdFromSecret(ctx, client, vendor, taxIdRef) -if err != nil { - return fmt.Errorf("failed to get tax ID: %w", err) -} -``` - -### Validating Secret Reference - -```go -// Validate that secret exists and contains the key -err := vendorsv1alpha1.ValidateTaxIdSecret(ctx, client, vendor, taxIdRef) -if err != nil { - return fmt.Errorf("invalid tax ID secret: %w", err) -} -``` - -### Creating/Updating Secrets - -```go -// Create new secret -err := vendorsv1alpha1.CreateTaxIdSecret(ctx, client, vendor, taxIdRef, "12-3456789") - -// Update existing secret -err := vendorsv1alpha1.UpdateTaxIdSecret(ctx, client, vendor, taxIdRef, "12-3456789") - -// Delete secret -err := vendorsv1alpha1.DeleteTaxIdSecret(ctx, client, vendor, taxIdRef) -``` - -## Best Practices - -### 1. **Secret Naming Convention** -Use descriptive names that include the vendor name: -```bash -# Good -acme-corp-tax-id -john-doe-ssn -international-corp-vat - -# Avoid -tax-secret-1 -secret-abc -``` - -### 2. **Key Naming Convention** -Use consistent key names within secrets: -```yaml -data: - tax-id: MTItMzQ1Njc4OQ== # For EIN - ssn: MTIzLTQ1LTY3ODk= # For SSN - vat-number: R0IxMjM0NTY3ODk= # For VAT -``` - -### 3. **Namespace Strategy** -- Use the same namespace as the vendor when possible -- Specify namespace explicitly for cross-namespace references -- Consider using a dedicated namespace for sensitive data - -### 4. **Labels and Annotations** -Add proper labels for identification and management: -```yaml -metadata: - labels: - vendor.miloapis.com/vendor: acme-corp - vendor.miloapis.com/type: tax-id - vendor.miloapis.com/country: us - annotations: - vendor.miloapis.com/tax-id-type: ein - vendor.miloapis.com/created-by: admin -``` - -### 5. **Owner References** -Set owner references to ensure cleanup: -```yaml -ownerReferences: - - apiVersion: vendors.miloapis.com/v1alpha1 - kind: Vendor - name: acme-corp - uid: 12345678-1234-1234-1234-123456789abc - controller: true -``` - -## Migration from Plain Text - -If you have existing vendors with plain text tax IDs: - -1. **Create secrets** for each vendor's tax ID -2. **Update vendor resources** to use `TaxIdRef` instead of `TaxId` -3. **Verify secrets** are accessible and contain correct data -4. **Test validation** to ensure everything works - -### Migration Script Example - -```bash -#!/bin/bash -# Extract tax IDs and create secrets -kubectl get vendors -o json | jq -r '.items[] | select(.spec.taxInfo.taxId) | "\(.metadata.name) \(.spec.taxInfo.taxId)"' | while read vendor_name tax_id; do - # Create secret - kubectl create secret generic "${vendor_name}-tax-id" \ - --from-literal=tax-id="$tax_id" \ - --namespace=default - - # Add labels - kubectl label secret "${vendor_name}-tax-id" \ - vendor.miloapis.com/vendor="$vendor_name" \ - vendor.miloapis.com/type=tax-id -done -``` - -## Security Considerations - -### 1. **RBAC Configuration** -Ensure proper RBAC for secret access: -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: vendor-secret-reader -rules: -- apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "list"] - resourceNames: ["*-tax-id"] # Restrict to tax ID secrets -``` - -### 2. **Network Policies** -Consider network policies to restrict secret access: -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: restrict-secret-access -spec: - podSelector: {} - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: vendor-controllers -``` - -### 3. **Audit Logging** -Enable audit logging for secret access: -```yaml -apiVersion: audit.k8s.io/v1 -kind: Policy -rules: -- level: Metadata - resources: - - group: "" - resources: ["secrets"] -``` - -## Troubleshooting - -### Common Issues - -1. **Secret Not Found** - ``` - Error: failed to get secret default/acme-corp-tax-id: secrets "acme-corp-tax-id" not found - ``` - - Check secret name and namespace - - Verify secret exists: `kubectl get secret acme-corp-tax-id` - -2. **Key Not Found** - ``` - Error: key tax-id not found in secret default/acme-corp-tax-id - ``` - - Check secret key name - - Verify key exists: `kubectl get secret acme-corp-tax-id -o yaml` - -3. **Permission Denied** - ``` - Error: secrets "acme-corp-tax-id" is forbidden: User cannot get resource "secrets" - ``` - - Check RBAC permissions - - Verify user has access to secrets - -### Debugging Commands - -```bash -# Check secret exists -kubectl get secret acme-corp-tax-id - -# View secret contents (base64 encoded) -kubectl get secret acme-corp-tax-id -o yaml - -# Decode secret value -kubectl get secret acme-corp-tax-id -o jsonpath='{.data.tax-id}' | base64 -d - -# Check secret labels -kubectl get secret acme-corp-tax-id --show-labels - -# Test secret access -kubectl auth can-i get secret acme-corp-tax-id -``` - -This approach ensures that sensitive tax ID information is properly secured while maintaining the flexibility and functionality of the vendor management system. diff --git a/docs/api/vendor-verification.md b/docs/api/vendor-verification.md index fb0f3d62..4b09813b 100644 --- a/docs/api/vendor-verification.md +++ b/docs/api/vendor-verification.md @@ -63,7 +63,6 @@ spec: status: completedAt: "2024-01-15T14:30:00Z" lastUpdatedAt: "2024-01-15T14:30:00Z" - attemptCount: 1 ``` ## Verification Types diff --git a/docs/api/vendors-api-group.md b/docs/api/vendors-api-group.md index 0c312865..7e918b90 100644 --- a/docs/api/vendors-api-group.md +++ b/docs/api/vendors-api-group.md @@ -152,22 +152,3 @@ config/ ├── vendor-example.yaml └── corporationtypeconfig-example.yaml ``` - -## Next Steps - -1. **Deploy the new CRDs** to your cluster -2. **Update any existing vendor resources** to use the new API group -3. **Update client applications** to reference `vendors.miloapis.com/v1alpha1` -4. **Consider adding vendor-specific controllers** for business logic -5. **Add vendor-specific webhooks** for validation and admission control - -## Backward Compatibility - -This is a breaking change that requires updating existing vendor resources to use the new API group. The resource structure remains the same, only the API group and version change. - -### Migration Script Example - -```bash -# Update existing vendor resources -kubectl get vendors -o yaml | sed 's/resourcemanager.miloapis.com/vendors.miloapis.com/g' | kubectl apply -f - -``` 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/vendors/verification_utils.go b/internal/controllers/vendors/verification_utils.go index cdc4c055..124d8e9b 100644 --- a/internal/controllers/vendors/verification_utils.go +++ b/internal/controllers/vendors/verification_utils.go @@ -188,9 +188,6 @@ func UpdateVerificationStatus(ctx context.Context, c client.Client, verification verification.Status.CompletedAt = &now } - // Increment attempt count - verification.Status.AttemptCount++ - return c.Update(ctx, verification) } diff --git a/pkg/apis/vendors/v1alpha1/vendorverification_types.go b/pkg/apis/vendors/v1alpha1/vendorverification_types.go index 25674b63..01d5c13f 100644 --- a/pkg/apis/vendors/v1alpha1/vendorverification_types.go +++ b/pkg/apis/vendors/v1alpha1/vendorverification_types.go @@ -117,10 +117,6 @@ type VendorVerificationSpec struct { // +kubebuilder:default=5 Priority int32 `json:"priority"` - // Whether this verification is required for vendor activation - // +kubebuilder:default=true - Required bool `json:"required"` - // Expiration date for this verification // +optional ExpirationDate *metav1.Time `json:"expirationDate,omitempty"` @@ -128,6 +124,10 @@ type VendorVerificationSpec struct { // 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 @@ -145,17 +145,13 @@ type VendorVerificationStatus struct { // +patchStrategy=merge Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` - // Timestamp when verification was completed - // +optional - CompletedAt *metav1.Time `json:"completedAt,omitempty"` - // Timestamp when verification was last updated // +optional LastUpdatedAt *metav1.Time `json:"lastUpdatedAt,omitempty"` - // Number of verification attempts + // Timestamp when verification was completed // +optional - AttemptCount int32 `json:"attemptCount,omitempty"` + CompletedAt *metav1.Time `json:"completedAt,omitempty"` // Last error message if verification failed // +optional diff --git a/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go index 4576bd57..30865b3a 100644 --- a/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/vendors/v1alpha1/zz_generated.deepcopy.go @@ -396,14 +396,14 @@ func (in *VendorVerificationStatus) DeepCopyInto(out *VendorVerificationStatus) (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.CompletedAt != nil { - in, out := &in.CompletedAt, &out.CompletedAt - *out = (*in).DeepCopy() - } 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.