• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valitydev / hellgate / 6323297423

27 Sep 2023 08:22AM UTC coverage: 90.143% (+0.004%) from 90.139%
6323297423

Pull #90

github

web-flow
Merge 8b69ceff8 into b8156b10e
Pull Request #90: OPS-358: Split hg invoice

171 of 171 new or added lines in 2 files covered. (100.0%)

4353 of 4829 relevant lines covered (90.14%)

583.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.83
/apps/hellgate/src/hg_invoice_handler.erl
1
-module(hg_invoice_handler).
2

3
-include("payment_events.hrl").
4
-include("invoice_events.hrl").
5
-include("domain.hrl").
6
-include("hg_invoice.hrl").
7

8
%% Woody handler called by hg_woody_service_wrapper
9

10
-behaviour(hg_woody_service_wrapper).
11

12
-export([handle_function/3]).
13

14
%% Internal
15

16
-import(hg_invoice_utils, [
17
    assert_party_operable/1,
18
    assert_shop_operable/1,
19
    assert_shop_exists/1
20
]).
21

22
%% API
23

24
-spec handle_function(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return().
25
handle_function(Func, Args, Opts) ->
26
    scoper:scope(
3,931✔
27
        invoicing,
28
        fun() ->
29
            handle_function_(Func, Args, Opts)
3,931✔
30
        end
31
    ).
32

33
-spec handle_function_(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return().
34
handle_function_('Create', {InvoiceParams}, _Opts) ->
35
    DomainRevision = hg_domain:head(),
216✔
36
    InvoiceID = InvoiceParams#payproc_InvoiceParams.id,
216✔
37
    _ = set_invoicing_meta(InvoiceID),
216✔
38
    PartyID = InvoiceParams#payproc_InvoiceParams.party_id,
216✔
39
    ShopID = InvoiceParams#payproc_InvoiceParams.shop_id,
216✔
40
    Party = hg_party:get_party(PartyID),
216✔
41
    Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)),
216✔
42
    _ = assert_party_shop_operable(Shop, Party),
215✔
43
    VS = #{
211✔
44
        cost => InvoiceParams#payproc_InvoiceParams.cost,
45
        shop_id => Shop#domain_Shop.id
46
    },
47
    MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS),
211✔
48
    ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms),
211✔
49
    AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation,
207✔
50
    Cost = InvoiceParams#payproc_InvoiceParams.cost,
207✔
51
    Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop),
207✔
52
    ok = ensure_started(InvoiceID, undefined, Party#domain_Party.revision, InvoiceParams, Allocation),
207✔
53
    get_invoice_state(get_state(InvoiceID));
207✔
54
handle_function_('CreateWithTemplate', {Params}, _Opts) ->
55
    DomainRevision = hg_domain:head(),
19✔
56
    InvoiceID = Params#payproc_InvoiceWithTemplateParams.id,
19✔
57
    _ = set_invoicing_meta(InvoiceID),
19✔
58
    TplID = Params#payproc_InvoiceWithTemplateParams.template_id,
19✔
59
    {Party, Shop, InvoiceParams} = make_invoice_params(Params),
19✔
60
    VS = #{
7✔
61
        cost => InvoiceParams#payproc_InvoiceParams.cost,
62
        shop_id => Shop#domain_Shop.id
63
    },
64
    MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS),
7✔
65
    ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms),
7✔
66
    AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation,
6✔
67
    Cost = InvoiceParams#payproc_InvoiceParams.cost,
6✔
68
    Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop),
6✔
69
    ok = ensure_started(InvoiceID, TplID, Party#domain_Party.revision, InvoiceParams, Allocation),
6✔
70
    get_invoice_state(get_state(InvoiceID));
6✔
71
handle_function_('CapturePaymentNew', Args, Opts) ->
72
    handle_function_('CapturePayment', Args, Opts);
×
73
handle_function_('Get', {InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}}, _Opts) ->
74
    _ = set_invoicing_meta(InvoiceID),
47✔
75
    St = get_state(InvoiceID, AfterID, Limit),
47✔
76
    get_invoice_state(St);
46✔
77
handle_function_('GetEvents', {InvoiceID, Range}, _Opts) ->
78
    _ = set_invoicing_meta(InvoiceID),
2,924✔
79
    get_public_history(InvoiceID, Range);
2,924✔
80
handle_function_('GetPayment', {InvoiceID, PaymentID}, _Opts) ->
81
    _ = set_invoicing_meta(InvoiceID, PaymentID),
271✔
82
    St = get_state(InvoiceID),
271✔
83
    get_payment_state(get_payment_session(PaymentID, St));
271✔
84
handle_function_('GetPaymentRefund', {InvoiceID, PaymentID, ID}, _Opts) ->
85
    _ = set_invoicing_meta(InvoiceID, PaymentID),
11✔
86
    St = get_state(InvoiceID),
11✔
87
    hg_invoice_payment:get_refund(ID, get_payment_session(PaymentID, St));
11✔
88
handle_function_('GetPaymentChargeback', {InvoiceID, PaymentID, ID}, _Opts) ->
89
    _ = set_invoicing_meta(InvoiceID, PaymentID),
×
90
    St = get_state(InvoiceID),
×
91
    CBSt = hg_invoice_payment:get_chargeback_state(ID, get_payment_session(PaymentID, St)),
×
92
    hg_invoice_payment_chargeback:get(CBSt);
×
93
handle_function_('GetPaymentAdjustment', {InvoiceID, PaymentID, ID}, _Opts) ->
94
    _ = set_invoicing_meta(InvoiceID, PaymentID),
6✔
95
    St = get_state(InvoiceID),
6✔
96
    hg_invoice_payment:get_adjustment(ID, get_payment_session(PaymentID, St));
6✔
97
handle_function_('ComputeTerms', {InvoiceID, PartyRevision0}, _Opts) ->
98
    _ = set_invoicing_meta(InvoiceID),
2✔
99
    St = get_state(InvoiceID),
2✔
100
    Timestamp = get_created_at(St),
2✔
101
    VS = hg_varset:prepare_shop_terms_varset(#{
2✔
102
        cost => get_cost(St)
103
    }),
104
    hg_invoice_utils:compute_shop_terms(
2✔
105
        get_party_id(St),
106
        get_shop_id(St),
107
        Timestamp,
108
        hg_maybe:get_defined(PartyRevision0, {timestamp, Timestamp}),
109
        VS
110
    );
111
handle_function_(Fun, Args, _Opts) when
112
    Fun =:= 'StartPayment' orelse
113
        Fun =:= 'RegisterPayment' orelse
114
        Fun =:= 'CapturePayment' orelse
115
        Fun =:= 'CancelPayment' orelse
116
        Fun =:= 'RefundPayment' orelse
117
        Fun =:= 'CreateManualRefund' orelse
118
        Fun =:= 'CreateChargeback' orelse
119
        Fun =:= 'CancelChargeback' orelse
120
        Fun =:= 'AcceptChargeback' orelse
121
        Fun =:= 'RejectChargeback' orelse
122
        Fun =:= 'ReopenChargeback' orelse
123
        Fun =:= 'CreatePaymentAdjustment' orelse
124
        Fun =:= 'Fulfill' orelse
125
        Fun =:= 'Rescind'
126
->
127
    InvoiceID = erlang:element(1, Args),
399✔
128
    _ = set_invoicing_meta(InvoiceID),
399✔
129
    call(InvoiceID, Fun, Args);
399✔
130
handle_function_('Repair', {InvoiceID, Changes, Action, Params}, _Opts) ->
131
    _ = set_invoicing_meta(InvoiceID),
12✔
132
    repair(InvoiceID, {changes, Changes, Action, Params});
12✔
133
handle_function_('RepairWithScenario', {InvoiceID, Scenario}, _Opts) ->
134
    _ = set_invoicing_meta(InvoiceID),
19✔
135
    repair(InvoiceID, {scenario, Scenario});
19✔
136
handle_function_('GetPaymentRoutesLimitValues', {InvoiceID, PaymentID}, _Opts) ->
137
    _ = set_invoicing_meta(InvoiceID, PaymentID),
5✔
138
    St = get_state(InvoiceID),
5✔
139
    hg_invoice_payment:get_limit_values(get_payment_session(PaymentID, St), hg_invoice:get_payment_opts(St)).
5✔
140

141
ensure_started(ID, TemplateID, PartyRevision, Params, Allocation) ->
142
    Invoice = hg_invoice:create(ID, TemplateID, PartyRevision, Params, Allocation),
213✔
143
    case hg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of
213✔
144
        {ok, _} -> ok;
209✔
145
        {error, exists} -> ok;
4✔
146
        {error, Reason} -> erlang:error(Reason)
×
147
    end.
148

149
call(ID, Function, Args) ->
150
    case hg_machine:thrift_call(hg_invoice:namespace(), ID, invoicing, {'Invoicing', Function}, Args) of
399✔
151
        ok -> ok;
65✔
152
        {ok, Reply} -> Reply;
258✔
153
        {exception, Exception} -> erlang:throw(Exception);
70✔
154
        {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{});
×
155
        {error, Error} -> erlang:error(Error)
6✔
156
    end.
157

158
repair(ID, Args) ->
159
    case hg_machine:repair(hg_invoice:namespace(), ID, Args) of
31✔
160
        {ok, _Result} -> ok;
24✔
161
        {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{});
×
162
        {error, working} -> erlang:throw(#base_InvalidRequest{errors = [<<"No need to repair">>]});
2✔
163
        {error, Reason} -> erlang:error(Reason)
5✔
164
    end.
165

166
maybe_allocation(undefined, _Cost, _MerchantTerms, _Party, _Shop) ->
167
    undefined;
210✔
168
maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop) ->
169
    PaymentTerms = MerchantTerms#domain_TermSet.payments,
3✔
170
    AllocationSelector = PaymentTerms#domain_PaymentsServiceTerms.allocations,
3✔
171
    case
3✔
172
        hg_allocation:calculate(
173
            AllocationPrototype,
174
            Party,
175
            Shop,
176
            Cost,
177
            AllocationSelector
178
        )
179
    of
180
        {ok, A} ->
181
            A;
3✔
182
        {error, allocation_not_allowed} ->
183
            throw(#payproc_AllocationNotAllowed{});
×
184
        {error, amount_exceeded} ->
185
            throw(#payproc_AllocationExceededPaymentAmount{});
×
186
        {error, {invalid_transaction, Transaction, Details}} ->
187
            throw(#payproc_AllocationInvalidTransaction{
×
188
                transaction = marshal_transaction(Transaction),
189
                reason = marshal_allocation_details(Details)
190
            })
191
    end.
192

193
marshal_transaction(#domain_AllocationTransaction{} = T) ->
194
    {transaction, T};
×
195
marshal_transaction(#domain_AllocationTransactionPrototype{} = TP) ->
196
    {transaction_prototype, TP}.
×
197

198
marshal_allocation_details(negative_amount) ->
199
    <<"Transaction amount is negative">>;
×
200
marshal_allocation_details(zero_amount) ->
201
    <<"Transaction amount is zero">>;
×
202
marshal_allocation_details(target_conflict) ->
203
    <<"Transaction with similar target">>;
×
204
marshal_allocation_details(currency_mismatch) ->
205
    <<"Transaction currency mismatch">>;
×
206
marshal_allocation_details(payment_institutions_mismatch) ->
207
    <<"Transaction target shop Payment Institution mismatch">>.
×
208

209
%%----------------- invoice asserts
210

211
assert_party_shop_operable(Shop, Party) ->
212
    _ = assert_party_operable(Party),
232✔
213
    _ = assert_shop_operable(Shop),
228✔
214
    ok.
224✔
215

216
get_invoice_state(#st{invoice = Invoice, payments = Payments}) ->
217
    #payproc_Invoice{
259✔
218
        invoice = Invoice,
219
        payments = [
220
            get_payment_state(PaymentSession)
46✔
221
         || {_PaymentID, PaymentSession} <- Payments
259✔
222
        ]
223
    }.
224

225
get_payment_state(PaymentSession) ->
226
    Refunds = hg_invoice_payment:get_refunds(PaymentSession),
317✔
227
    LegacyRefunds =
317✔
228
        lists:map(
229
            fun(#payproc_InvoicePaymentRefund{refund = R}) ->
230
                R
13✔
231
            end,
232
            Refunds
233
        ),
234
    #payproc_InvoicePayment{
317✔
235
        payment = hg_invoice_payment:get_payment(PaymentSession),
236
        adjustments = hg_invoice_payment:get_adjustments(PaymentSession),
237
        chargebacks = hg_invoice_payment:get_chargebacks(PaymentSession),
238
        route = hg_invoice_payment:get_route(PaymentSession),
239
        cash_flow = hg_invoice_payment:get_final_cashflow(PaymentSession),
240
        legacy_refunds = LegacyRefunds,
241
        refunds = Refunds,
242
        sessions = hg_invoice_payment:get_sessions(PaymentSession),
243
        last_transaction_info = hg_invoice_payment:get_trx(PaymentSession),
244
        allocation = hg_invoice_payment:get_allocation(PaymentSession)
245
    }.
246

247
set_invoicing_meta(InvoiceID) ->
248
    scoper:add_meta(#{invoice_id => InvoiceID}).
3,638✔
249

250
set_invoicing_meta(InvoiceID, PaymentID) ->
251
    scoper:add_meta(#{invoice_id => InvoiceID, payment_id => PaymentID}).
293✔
252

253
%%
254

255
get_state(ID) ->
256
    hg_invoice:collapse_history(get_history(ID)).
508✔
257

258
get_state(ID, AfterID, Limit) ->
259
    hg_invoice:collapse_history(get_history(ID, AfterID, Limit)).
47✔
260

261
get_history(ID) ->
262
    History = hg_machine:get_history(hg_invoice:namespace(), ID),
508✔
263
    hg_invoice:unmarshal_history(map_history_error(History)).
508✔
264

265
get_history(ID, AfterID, Limit) ->
266
    History = hg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit),
2,971✔
267
    hg_invoice:unmarshal_history(map_history_error(History)).
2,971✔
268

269
get_public_history(InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}) ->
270
    [publish_invoice_event(InvoiceID, Ev) || Ev <- get_history(InvoiceID, AfterID, Limit)].
2,924✔
271

272
publish_invoice_event(InvoiceID, {ID, Dt, Event}) ->
273
    #payproc_Event{
2,411✔
274
        id = ID,
275
        source = {invoice_id, InvoiceID},
276
        created_at = Dt,
277
        payload = ?invoice_ev(Event)
278
    }.
279

280
map_history_error({ok, Result}) ->
281
    Result;
3,478✔
282
map_history_error({error, notfound}) ->
283
    throw(#payproc_InvoiceNotFound{}).
1✔
284

285
%%
286

287
get_party_id(#st{invoice = #domain_Invoice{owner_id = PartyID}}) ->
288
    PartyID.
2✔
289

290
get_shop_id(#st{invoice = #domain_Invoice{shop_id = ShopID}}) ->
291
    ShopID.
2✔
292

293
get_created_at(#st{invoice = #domain_Invoice{created_at = CreatedAt}}) ->
294
    CreatedAt.
2✔
295

296
get_cost(#st{invoice = #domain_Invoice{cost = Cash}}) ->
297
    Cash.
2✔
298

299
get_payment_session(PaymentID, St) ->
300
    case try_get_payment_session(PaymentID, St) of
293✔
301
        PaymentSession when PaymentSession /= undefined ->
302
            PaymentSession;
293✔
303
        undefined ->
304
            throw(#payproc_InvoicePaymentNotFound{})
×
305
    end.
306

307
try_get_payment_session(PaymentID, #st{payments = Payments}) ->
308
    case lists:keyfind(PaymentID, 1, Payments) of
293✔
309
        {PaymentID, PaymentSession} ->
310
            PaymentSession;
293✔
311
        false ->
312
            undefined
×
313
    end.
314

315
%%
316

317
make_invoice_params(Params) ->
318
    #payproc_InvoiceWithTemplateParams{
19✔
319
        id = InvoiceID,
320
        template_id = TplID,
321
        cost = Cost,
322
        context = Context,
323
        external_id = ExternalID
324
    } = Params,
325
    #domain_InvoiceTemplate{
19✔
326
        owner_id = PartyID,
327
        shop_id = ShopID,
328
        invoice_lifetime = Lifetime,
329
        product = Product,
330
        description = Description,
331
        details = TplDetails,
332
        context = TplContext
333
    } = hg_invoice_template:get(TplID),
334
    Party = hg_party:get_party(PartyID),
17✔
335
    Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)),
17✔
336
    _ = assert_party_shop_operable(Shop, Party),
17✔
337
    Cart = make_invoice_cart(Cost, TplDetails, Shop),
13✔
338
    InvoiceDetails = #domain_InvoiceDetails{
7✔
339
        product = Product,
340
        description = Description,
341
        cart = Cart
342
    },
343
    InvoiceCost = hg_invoice_utils:get_cart_amount(Cart),
7✔
344
    InvoiceDue = make_invoice_due_date(Lifetime),
7✔
345
    InvoiceContext = make_invoice_context(Context, TplContext),
7✔
346
    InvoiceParams = #payproc_InvoiceParams{
7✔
347
        id = InvoiceID,
348
        party_id = PartyID,
349
        shop_id = ShopID,
350
        details = InvoiceDetails,
351
        due = InvoiceDue,
352
        cost = InvoiceCost,
353
        context = InvoiceContext,
354
        external_id = ExternalID
355
    },
356
    {Party, Shop, InvoiceParams}.
7✔
357

358
validate_invoice_params(#payproc_InvoiceParams{cost = Cost}, Shop, MerchantTerms) ->
359
    ok = validate_invoice_cost(Cost, Shop, MerchantTerms),
218✔
360
    ok.
213✔
361

362
validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) ->
363
    _ = hg_invoice_utils:validate_cost(Cost, Shop),
218✔
364
    _ = hg_invoice_utils:assert_cost_payable(Cost, PaymentTerms),
216✔
365
    ok.
213✔
366

367
make_invoice_cart(_, {cart, Cart}, _Shop) ->
368
    Cart;
×
369
make_invoice_cart(Cost, {product, TplProduct}, Shop) ->
370
    #domain_InvoiceTemplateProduct{
13✔
371
        product = Product,
372
        price = TplPrice,
373
        metadata = Metadata
374
    } = TplProduct,
375
    #domain_InvoiceCart{
13✔
376
        lines = [
377
            #domain_InvoiceLine{
378
                product = Product,
379
                quantity = 1,
380
                price = get_templated_price(Cost, TplPrice, Shop),
381
                metadata = Metadata
382
            }
383
        ]
384
    }.
385

386
get_templated_price(undefined, {fixed, Cost}, Shop) ->
387
    get_cost(Cost, Shop);
2✔
388
get_templated_price(undefined, _, _Shop) ->
389
    throw(#base_InvalidRequest{errors = [?INVOICE_TPL_NO_COST]});
1✔
390
get_templated_price(Cost, {fixed, Cost}, Shop) ->
391
    get_cost(Cost, Shop);
3✔
392
get_templated_price(_Cost, {fixed, _CostTpl}, _Shop) ->
393
    throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_COST]});
2✔
394
get_templated_price(Cost, {range, Range}, Shop) ->
395
    _ = assert_cost_in_range(Cost, Range),
4✔
396
    get_cost(Cost, Shop);
1✔
397
get_templated_price(Cost, {unlim, _}, Shop) ->
398
    get_cost(Cost, Shop).
1✔
399

400
get_cost(Cost, Shop) ->
401
    ok = hg_invoice_utils:validate_cost(Cost, Shop),
7✔
402
    Cost.
7✔
403

404
assert_cost_in_range(
405
    #domain_Cash{amount = Amount, currency = Currency},
406
    #domain_CashRange{
407
        upper = {UType, #domain_Cash{amount = UAmount, currency = Currency}},
408
        lower = {LType, #domain_Cash{amount = LAmount, currency = Currency}}
409
    }
410
) ->
411
    _ = assert_less_than(LType, LAmount, Amount),
3✔
412
    _ = assert_less_than(UType, Amount, UAmount),
2✔
413
    ok;
1✔
414
assert_cost_in_range(_, _) ->
415
    throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_CURRENCY]}).
1✔
416

417
assert_less_than(inclusive, Less, More) when Less =< More ->
418
    ok;
3✔
419
assert_less_than(exclusive, Less, More) when Less < More ->
420
    ok;
×
421
assert_less_than(_, _, _) ->
422
    throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_AMOUNT]}).
2✔
423

424
make_invoice_due_date(#domain_LifetimeInterval{years = YY, months = MM, days = DD}) ->
425
    hg_datetime:add_interval(hg_datetime:format_now(), {YY, MM, DD}).
7✔
426

427
make_invoice_context(undefined, TplContext) ->
428
    TplContext;
3✔
429
make_invoice_context(Context, _) ->
430
    Context.
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc