src/finanza@interest@amortization.erl

-module(finanza@interest@amortization).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/finanza/interest/amortization.gleam").
-export([index/1, payment/1, interest/1, principal_paid/1, balance/1, schedule/4]).
-export_type([period/0, schedule_state/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Amortization schedule generation for an amortising loan.\n"
    "\n"
    " The schedule lists, for each period, the periodic payment, the\n"
    " portion of that payment that goes to interest, the portion that\n"
    " reduces principal, and the resulting outstanding balance.\n"
).

-opaque period() :: {period,
        integer(),
        finanza@decimal:decimal(),
        finanza@decimal:decimal(),
        finanza@decimal:decimal(),
        finanza@decimal:decimal()}.

-type schedule_state() :: {schedule_state,
        list(period()),
        finanza@decimal:decimal()}.

-file("src/finanza/interest/amortization.gleam", 29).
?DOC(" 1-based period number.\n").
-spec index(period()) -> integer().
index(P) ->
    erlang:element(2, P).

-file("src/finanza/interest/amortization.gleam", 34).
?DOC(" Total payment for this period.\n").
-spec payment(period()) -> finanza@decimal:decimal().
payment(P) ->
    erlang:element(3, P).

-file("src/finanza/interest/amortization.gleam", 39).
?DOC(" Interest component of the payment.\n").
-spec interest(period()) -> finanza@decimal:decimal().
interest(P) ->
    erlang:element(4, P).

-file("src/finanza/interest/amortization.gleam", 44).
?DOC(" Principal component of the payment.\n").
-spec principal_paid(period()) -> finanza@decimal:decimal().
principal_paid(P) ->
    erlang:element(5, P).

-file("src/finanza/interest/amortization.gleam", 49).
?DOC(" Remaining balance after this period.\n").
-spec balance(period()) -> finanza@decimal:decimal().
balance(P) ->
    erlang:element(6, P).

-file("src/finanza/interest/amortization.gleam", 139).
-spec finalise_row(
    integer(),
    integer(),
    finanza@decimal:decimal(),
    finanza@decimal:decimal(),
    finanza@decimal:decimal(),
    finanza@decimal:decimal(),
    integer()
) -> {ok,
        {finanza@decimal:decimal(),
            finanza@decimal:decimal(),
            finanza@decimal:decimal()}} |
    {error, finanza@interest:interest_error()}.
finalise_row(Index, Periods, Payment, Principal, Balance, Interest, Digits) ->
    gleam@bool:guard(
        Index /= Periods,
        {ok, {Payment, Principal, Balance}},
        fun() ->
            Target_zero = finanza@decimal:new(0, - Digits),
            gleam@result:'try'(
                begin
                    _pipe = finanza@decimal:add(Principal, Balance),
                    gleam@result:map_error(
                        _pipe,
                        fun(Field@0) -> {arithmetic_error, Field@0} end
                    )
                end,
                fun(Adjusted_principal) ->
                    gleam@result:map(
                        begin
                            _pipe@1 = finanza@decimal:add(
                                Interest,
                                Adjusted_principal
                            ),
                            gleam@result:map_error(
                                _pipe@1,
                                fun(Field@0) -> {arithmetic_error, Field@0} end
                            )
                        end,
                        fun(Adjusted_payment) ->
                            {Adjusted_payment, Adjusted_principal, Target_zero}
                        end
                    )
                end
            )
        end
    ).

-file("src/finanza/interest/amortization.gleam", 87).
-spec build_schedule(
    finanza@decimal:decimal(),
    finanza@decimal:decimal(),
    integer(),
    integer(),
    integer(),
    schedule_state()
) -> {ok, schedule_state()} | {error, finanza@interest:interest_error()}.
build_schedule(Payment, Rate, Periods, Digits, Index, State) ->
    gleam@bool:guard(
        Index > Periods,
        {ok, State},
        fun() ->
            gleam@result:'try'(
                begin
                    _pipe = finanza@decimal:multiply(
                        erlang:element(3, State),
                        Rate
                    ),
                    gleam@result:map_error(
                        _pipe,
                        fun(Field@0) -> {arithmetic_error, Field@0} end
                    )
                end,
                fun(Raw_interest) ->
                    Rounded_interest = finanza@decimal:round(
                        Raw_interest,
                        Digits,
                        half_even
                    ),
                    gleam@result:'try'(
                        begin
                            _pipe@1 = finanza@decimal:subtract(
                                Payment,
                                Rounded_interest
                            ),
                            gleam@result:map_error(
                                _pipe@1,
                                fun(Field@0) -> {arithmetic_error, Field@0} end
                            )
                        end,
                        fun(Principal_part) ->
                            gleam@result:'try'(
                                begin
                                    _pipe@2 = finanza@decimal:subtract(
                                        erlang:element(3, State),
                                        Principal_part
                                    ),
                                    gleam@result:map_error(
                                        _pipe@2,
                                        fun(Field@0) -> {arithmetic_error, Field@0} end
                                    )
                                end,
                                fun(New_balance) ->
                                    gleam@result:'try'(
                                        finalise_row(
                                            Index,
                                            Periods,
                                            Payment,
                                            Principal_part,
                                            New_balance,
                                            Rounded_interest,
                                            Digits
                                        ),
                                        fun(_use0) ->
                                            {Final_payment,
                                                Final_principal,
                                                Final_balance} = _use0,
                                            Row = {period,
                                                Index,
                                                Final_payment,
                                                Rounded_interest,
                                                Final_principal,
                                                Final_balance},
                                            build_schedule(
                                                Payment,
                                                Rate,
                                                Periods,
                                                Digits,
                                                Index + 1,
                                                {schedule_state,
                                                    [Row |
                                                        erlang:element(2, State)],
                                                    Final_balance}
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/finanza/interest/amortization.gleam", 59).
?DOC(
    " Build a full amortisation schedule for an amortising loan.\n"
    "\n"
    " Each row in the returned list is a [`Period`](#Period). All\n"
    " monetary values are rounded to `digits` decimal places with\n"
    " `HalfEven`. The final row is adjusted so the outstanding balance\n"
    " closes to zero exactly.\n"
).
-spec schedule(
    finanza@decimal:decimal(),
    finanza@decimal:decimal(),
    integer(),
    integer()
) -> {ok, list(period())} | {error, finanza@interest:interest_error()}.
schedule(Principal, Rate_per_period, Periods, Digits) ->
    gleam@result:'try'(
        finanza@interest:payment(Principal, Rate_per_period, Periods, Digits),
        fun(Payment_amount) ->
            Initial = {schedule_state, [], Principal},
            gleam@result:map(
                build_schedule(
                    Payment_amount,
                    Rate_per_period,
                    Periods,
                    Digits,
                    1,
                    Initial
                ),
                fun(Built) -> lists:reverse(erlang:element(2, Built)) end
            )
        end
    ).