|
8 | 8 | "github.com/openmeterio/openmeter/openmeter/billing/charges/flatfee" |
9 | 9 | "github.com/openmeterio/openmeter/openmeter/billing/charges/meta" |
10 | 10 | "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| 11 | + "github.com/openmeterio/openmeter/pkg/clock" |
11 | 12 | "github.com/openmeterio/openmeter/pkg/framework/transaction" |
12 | 13 | "github.com/openmeterio/openmeter/pkg/models" |
13 | 14 | ) |
@@ -45,6 +46,26 @@ func (s *service) TriggerPatch(ctx context.Context, chargeID meta.ChargeID, patc |
45 | 46 | var result meta.TriggerPatchResult[flatfee.Charge] |
46 | 47 |
|
47 | 48 | charge, err := s.withLockedCharge(ctx, chargeID, func(ctx context.Context, charge flatfee.Charge) (*flatfee.Charge, error) { |
| 49 | + chargeWithUpdatedBase, err := applyBaseIntentPatchForOverriddenCharge(charge, patch) |
| 50 | + if err != nil { |
| 51 | + return nil, err |
| 52 | + } |
| 53 | + |
| 54 | + if chargeWithUpdatedBase != nil { |
| 55 | + // Hidden base/source intent changes are subscription reconciliation, |
| 56 | + // not customer-facing lifecycle events. Persist the source intent and |
| 57 | + // skip the state machine because the active override owns lifecycle |
| 58 | + // state and hidden targets are rejected there. |
| 59 | + updatedChargeBase, err := s.adapter.UpdateCharge(ctx, chargeWithUpdatedBase.ChargeBase) |
| 60 | + if err != nil { |
| 61 | + return nil, fmt.Errorf("updating flat fee charge[%s] base intent: %w", chargeWithUpdatedBase.ID, err) |
| 62 | + } |
| 63 | + |
| 64 | + chargeWithUpdatedBase.ChargeBase = updatedChargeBase |
| 65 | + |
| 66 | + return chargeWithUpdatedBase, nil |
| 67 | + } |
| 68 | + |
48 | 69 | stateMachine, err := s.newStateMachine(StateMachineConfig{ |
49 | 70 | Charge: charge, |
50 | 71 | Adapter: s.adapter, |
@@ -75,6 +96,65 @@ func (s *service) TriggerPatch(ctx context.Context, chargeID meta.ChargeID, patc |
75 | 96 | return result, nil |
76 | 97 | } |
77 | 98 |
|
| 99 | +func applyBaseIntentPatchForOverriddenCharge(charge flatfee.Charge, patch meta.Patch) (*flatfee.Charge, error) { |
| 100 | + target, err := patch.GetTargetLayer(charge.Intent) |
| 101 | + if err != nil { |
| 102 | + return nil, fmt.Errorf("getting patch target layer: %w", err) |
| 103 | + } |
| 104 | + |
| 105 | + if target != meta.ChangeTargetBase || !charge.Intent.HasOverrideLayer() { |
| 106 | + return nil, nil |
| 107 | + } |
| 108 | + |
| 109 | + switch patch := patch.(type) { |
| 110 | + case meta.PatchDelete: |
| 111 | + if err := charge.Intent.Mutate(meta.ChangeTargetBase, func(fields *flatfee.IntentMutableFields) { |
| 112 | + deletedAt := clock.Now() |
| 113 | + fields.IntentDeletedAt = &deletedAt |
| 114 | + }); err != nil { |
| 115 | + return nil, fmt.Errorf("mutating base intent for %s patch: %w", patch.Op(), err) |
| 116 | + } |
| 117 | + |
| 118 | + return &charge, nil |
| 119 | + case meta.PatchShrink: |
| 120 | + if err := mutateBaseIntentPeriodForOverriddenCharge(&charge, patch); err != nil { |
| 121 | + return nil, err |
| 122 | + } |
| 123 | + |
| 124 | + return &charge, nil |
| 125 | + case meta.PatchExtend: |
| 126 | + if err := mutateBaseIntentPeriodForOverriddenCharge(&charge, patch); err != nil { |
| 127 | + return nil, err |
| 128 | + } |
| 129 | + |
| 130 | + return &charge, nil |
| 131 | + } |
| 132 | + |
| 133 | + return nil, nil |
| 134 | +} |
| 135 | + |
| 136 | +func mutateBaseIntentPeriodForOverriddenCharge(charge *flatfee.Charge, patch periodPatch) error { |
| 137 | + targetIntent, err := charge.Intent.GetIntentForTarget(meta.ChangeTargetBase) |
| 138 | + if err != nil { |
| 139 | + return fmt.Errorf("getting base intent: %w", err) |
| 140 | + } |
| 141 | + |
| 142 | + if err := patch.ValidateWith(targetIntent.IntentMutableFields.IntentMutableFields); err != nil { |
| 143 | + return fmt.Errorf("validate %s patch: %w", patch.Op(), err) |
| 144 | + } |
| 145 | + |
| 146 | + if err := charge.Intent.Mutate(meta.ChangeTargetBase, func(fields *flatfee.IntentMutableFields) { |
| 147 | + fields.ServicePeriod.To = patch.GetNewServicePeriodTo() |
| 148 | + fields.FullServicePeriod.To = patch.GetNewFullServicePeriodTo() |
| 149 | + fields.BillingPeriod.To = patch.GetNewBillingPeriodTo() |
| 150 | + fields.InvoiceAt = patch.GetNewInvoiceAt() |
| 151 | + }); err != nil { |
| 152 | + return fmt.Errorf("mutating base intent for %s patch: %w", patch.Op(), err) |
| 153 | + } |
| 154 | + |
| 155 | + return nil |
| 156 | +} |
| 157 | + |
78 | 158 | func (s *service) newStateMachine(config StateMachineConfig) (StateMachine, error) { |
79 | 159 | switch config.Charge.Intent.GetSettlementMode() { |
80 | 160 | case productcatalog.CreditOnlySettlementMode: |
|
0 commit comments