|
1 | 1 | package hooks_test |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "context" |
5 | 4 | "testing" |
6 | 5 |
|
| 6 | + decimal "github.com/alpacahq/alpacadecimal" |
| 7 | + "github.com/samber/lo" |
7 | 8 | "github.com/stretchr/testify/require" |
8 | 9 |
|
9 | | - "github.com/openmeterio/openmeter/openmeter/productcatalog/plan" |
| 10 | + "github.com/openmeterio/openmeter/openmeter/app" |
| 11 | + "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| 12 | + pctestutils "github.com/openmeterio/openmeter/openmeter/productcatalog/testutils" |
10 | 13 | "github.com/openmeterio/openmeter/openmeter/taxcode" |
11 | 14 | "github.com/openmeterio/openmeter/openmeter/taxcode/service/hooks" |
12 | 15 | "github.com/openmeterio/openmeter/pkg/models" |
13 | | - "github.com/openmeterio/openmeter/pkg/pagination" |
14 | 16 | ) |
15 | 17 |
|
16 | | -// stubPlanService implements plan.Service for testing the plan hook. |
17 | | -// Only ListPlans has real behavior; all other methods panic. |
18 | | -type stubPlanService struct { |
19 | | - listResult pagination.Result[plan.Plan] |
20 | | - listErr error |
21 | | - lastInput plan.ListPlansInput |
22 | | -} |
23 | | - |
24 | | -func (s *stubPlanService) ListPlans(ctx context.Context, params plan.ListPlansInput) (pagination.Result[plan.Plan], error) { |
25 | | - s.lastInput = params |
26 | | - return s.listResult, s.listErr |
27 | | -} |
28 | | - |
29 | | -func (s *stubPlanService) CreatePlan(_ context.Context, _ plan.CreatePlanInput) (*plan.Plan, error) { |
30 | | - panic("not implemented") |
31 | | -} |
32 | | - |
33 | | -func (s *stubPlanService) DeletePlan(_ context.Context, _ plan.DeletePlanInput) error { |
34 | | - panic("not implemented") |
35 | | -} |
36 | | - |
37 | | -func (s *stubPlanService) GetPlan(_ context.Context, _ plan.GetPlanInput) (*plan.Plan, error) { |
38 | | - panic("not implemented") |
39 | | -} |
| 18 | +// setupNamespaceDefaults provisions the organisation-default tax codes that |
| 19 | +// DeleteTaxCode requires to exist before it calls the pre-delete hook. |
| 20 | +func setupNamespaceDefaults(t *testing.T, env *pctestutils.TestEnv, ns string) { |
| 21 | + t.Helper() |
40 | 22 |
|
41 | | -func (s *stubPlanService) UpdatePlan(_ context.Context, _ plan.UpdatePlanInput) (*plan.Plan, error) { |
42 | | - panic("not implemented") |
43 | | -} |
44 | | - |
45 | | -func (s *stubPlanService) PublishPlan(_ context.Context, _ plan.PublishPlanInput) (*plan.Plan, error) { |
46 | | - panic("not implemented") |
47 | | -} |
| 23 | + invoicing, err := env.TaxCode.CreateTaxCode(t.Context(), taxcode.CreateTaxCodeInput{ |
| 24 | + Namespace: ns, |
| 25 | + Key: "default-invoicing", |
| 26 | + Name: "Provider Default", |
| 27 | + }) |
| 28 | + require.NoError(t, err) |
| 29 | + |
| 30 | + creditGrant, err := env.TaxCode.CreateTaxCode(t.Context(), taxcode.CreateTaxCodeInput{ |
| 31 | + Namespace: ns, |
| 32 | + Key: "default-credit-grant", |
| 33 | + Name: "Non-Taxable", |
| 34 | + AppMappings: taxcode.TaxCodeAppMappings{ |
| 35 | + {AppType: app.AppTypeStripe, TaxCode: "txcd_00000000"}, |
| 36 | + }, |
| 37 | + }) |
| 38 | + require.NoError(t, err) |
48 | 39 |
|
49 | | -func (s *stubPlanService) ArchivePlan(_ context.Context, _ plan.ArchivePlanInput) (*plan.Plan, error) { |
50 | | - panic("not implemented") |
| 40 | + _, err = env.TaxCode.UpsertOrganizationDefaultTaxCodes(t.Context(), taxcode.UpsertOrganizationDefaultTaxCodesInput{ |
| 41 | + Namespace: ns, |
| 42 | + InvoicingTaxCodeID: invoicing.ID, |
| 43 | + CreditGrantTaxCodeID: creditGrant.ID, |
| 44 | + }) |
| 45 | + require.NoError(t, err) |
51 | 46 | } |
52 | 47 |
|
53 | | -func (s *stubPlanService) NextPlan(_ context.Context, _ plan.NextPlanInput) (*plan.Plan, error) { |
54 | | - panic("not implemented") |
55 | | -} |
| 48 | +func TestPlanHookPreDelete(t *testing.T) { |
| 49 | + // Setup real services backed by Postgres. |
| 50 | + env := pctestutils.NewTestEnv(t) |
| 51 | + t.Cleanup(func() { env.Close(t) }) |
| 52 | + env.DBSchemaMigrate(t) |
56 | 53 |
|
57 | | -var _ plan.Service = (*stubPlanService)(nil) |
| 54 | + // Register the plan hook on the real taxcode service. |
| 55 | + planHook, err := hooks.NewPlanHook(hooks.PlanHookConfig{PlanService: env.Plan}) |
| 56 | + require.NoError(t, err) |
| 57 | + env.TaxCode.RegisterHooks(planHook) |
58 | 58 |
|
59 | | -const testTaxCodeID = "01234567890123456789012345" |
| 59 | + ns := pctestutils.NewTestNamespace(t) |
60 | 60 |
|
61 | | -func TestPlanHook_PreDelete(t *testing.T) { |
62 | | - tc := &taxcode.TaxCode{ |
63 | | - NamespacedID: models.NamespacedID{ |
64 | | - Namespace: "test-ns", |
65 | | - ID: testTaxCodeID, |
66 | | - }, |
67 | | - } |
| 61 | + // Provision organisation-default tax codes so DeleteTaxCode can proceed past |
| 62 | + // the org-defaults check and reach the pre-delete hook. |
| 63 | + setupNamespaceDefaults(t, env, ns) |
68 | 64 |
|
69 | 65 | t.Run("blocks deletion when a plan references the tax code", func(t *testing.T) { |
70 | | - // given: a plan service that returns one matching plan |
71 | | - stub := &stubPlanService{ |
72 | | - listResult: pagination.Result[plan.Plan]{ |
73 | | - Items: []plan.Plan{ |
74 | | - {ManagedModel: models.ManagedModel{}, NamespacedID: models.NamespacedID{ID: "plan-abc"}}, |
75 | | - }, |
76 | | - TotalCount: 1, |
| 66 | + // given: a tax code that a plan will reference |
| 67 | + referenced, err := env.TaxCode.CreateTaxCode(t.Context(), taxcode.CreateTaxCodeInput{ |
| 68 | + Namespace: ns, |
| 69 | + Key: "referenced", |
| 70 | + Name: "Referenced Tax Code", |
| 71 | + AppMappings: taxcode.TaxCodeAppMappings{ |
| 72 | + {AppType: app.AppTypeStripe, TaxCode: "txcd_20000001"}, |
77 | 73 | }, |
78 | | - } |
| 74 | + }) |
| 75 | + require.NoError(t, err) |
79 | 76 |
|
80 | | - hook, err := hooks.NewPlanHook(hooks.PlanHookConfig{PlanService: stub}) |
| 77 | + // given: a plan whose rate card references the tax code |
| 78 | + planInput := pctestutils.NewTestPlan(t, ns, |
| 79 | + pctestutils.WithPlanKey("plan-with-taxcode"), |
| 80 | + pctestutils.WithPlanPhases(productcatalog.Phase{ |
| 81 | + PhaseMeta: productcatalog.PhaseMeta{ |
| 82 | + Key: "default", |
| 83 | + Name: "Default", |
| 84 | + }, |
| 85 | + RateCards: []productcatalog.RateCard{ |
| 86 | + &productcatalog.FlatFeeRateCard{ |
| 87 | + RateCardMeta: productcatalog.RateCardMeta{ |
| 88 | + Key: "rc-1", |
| 89 | + Name: "RC 1", |
| 90 | + TaxConfig: &productcatalog.TaxConfig{ |
| 91 | + TaxCodeID: lo.ToPtr(referenced.ID), |
| 92 | + }, |
| 93 | + Price: productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| 94 | + Amount: decimal.NewFromInt(0), |
| 95 | + PaymentTerm: productcatalog.InArrearsPaymentTerm, |
| 96 | + }), |
| 97 | + }, |
| 98 | + BillingCadence: &pctestutils.MonthPeriod, |
| 99 | + }, |
| 100 | + }, |
| 101 | + }), |
| 102 | + ) |
| 103 | + _, err = env.Plan.CreatePlan(t.Context(), planInput) |
81 | 104 | require.NoError(t, err) |
82 | 105 |
|
83 | | - // when: PreDelete is called |
84 | | - err = hook.PreDelete(t.Context(), tc) |
| 106 | + // when: attempting to delete the referenced tax code |
| 107 | + err = env.TaxCode.DeleteTaxCode(t.Context(), taxcode.DeleteTaxCodeInput{ |
| 108 | + NamespacedID: models.NamespacedID{Namespace: ns, ID: referenced.ID}, |
| 109 | + }) |
85 | 110 |
|
86 | 111 | // then: an error is returned and it is a TaxCodeReferencedByPlan error |
87 | 112 | require.Error(t, err) |
88 | 113 | require.True(t, taxcode.IsTaxCodeReferencedByPlanError(err), |
89 | 114 | "expected TaxCodeReferencedByPlan error, got: %v", err) |
90 | | - |
91 | | - // and: the stub received a ListPlansInput whose TaxCodes.In contains the tax code id |
92 | | - require.NotNil(t, stub.lastInput.TaxCodes) |
93 | | - require.NotNil(t, stub.lastInput.TaxCodes.In) |
94 | | - require.Contains(t, *stub.lastInput.TaxCodes.In, testTaxCodeID) |
95 | 115 | }) |
96 | 116 |
|
97 | 117 | t.Run("allows deletion when no plan references the tax code", func(t *testing.T) { |
98 | | - // given: a plan service that returns no matching plans |
99 | | - stub := &stubPlanService{ |
100 | | - listResult: pagination.Result[plan.Plan]{ |
101 | | - Items: []plan.Plan{}, |
102 | | - TotalCount: 0, |
| 118 | + // given: a tax code that no plan references |
| 119 | + unreferenced, err := env.TaxCode.CreateTaxCode(t.Context(), taxcode.CreateTaxCodeInput{ |
| 120 | + Namespace: ns, |
| 121 | + Key: "unreferenced", |
| 122 | + Name: "Unreferenced Tax Code", |
| 123 | + AppMappings: taxcode.TaxCodeAppMappings{ |
| 124 | + {AppType: app.AppTypeStripe, TaxCode: "txcd_20000002"}, |
103 | 125 | }, |
104 | | - } |
105 | | - |
106 | | - hook, err := hooks.NewPlanHook(hooks.PlanHookConfig{PlanService: stub}) |
| 126 | + }) |
107 | 127 | require.NoError(t, err) |
108 | 128 |
|
109 | | - // when: PreDelete is called |
110 | | - err = hook.PreDelete(t.Context(), tc) |
| 129 | + // when: deleting the unreferenced tax code |
| 130 | + err = env.TaxCode.DeleteTaxCode(t.Context(), taxcode.DeleteTaxCodeInput{ |
| 131 | + NamespacedID: models.NamespacedID{Namespace: ns, ID: unreferenced.ID}, |
| 132 | + }) |
111 | 133 |
|
112 | 134 | // then: no error is returned |
113 | 135 | require.NoError(t, err) |
|
0 commit comments