Current section

Files

Jump to
telega src telega@payments.erl
Raw

src/telega@payments.erl

-module(telega@payments).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega/payments.gleam").
-export([price/2, stars_invoice/4, invoice/6, with_photo/2, with_tips/3, with_flexible_shipping/1, with_provider_data/2, with_start_parameter/2, with_reply_markup/2, require_name/1, require_phone_number/1, require_email/1, send/2, create_link/2, answer_pre_checkout_ok/2, answer_pre_checkout_error/3, shipping_option/3, answer_shipping_ok/3, answer_shipping_error/3, wait_successful_payment/4]).
-export_type([invoice/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(
" Helpers for sending invoices and answering payment queries, on top of\n"
" the raw Bot API methods in `telega/api`.\n"
"\n"
" ## Telegram Stars\n"
"\n"
" [Telegram Stars](https://core.telegram.org/bots/payments-stars) (`XTR`)\n"
" are the first-class case: digital goods and services must be sold in\n"
" Stars, no payment provider is required, and the invoice has a single\n"
" price.\n"
"\n"
" ```gleam\n"
" import telega/payments\n"
"\n"
" fn buy_handler(ctx, _command) {\n"
" let assert Ok(_) =\n"
" payments.stars_invoice(\n"
" title: \"Premium\",\n"
" description: \"Premium access for a month\",\n"
" payload: \"premium:1m\",\n"
" amount: 100,\n"
" )\n"
" |> payments.send(ctx)\n"
" Ok(ctx)\n"
" }\n"
" ```\n"
"\n"
" `payload` is not shown to the user — it comes back in the pre-checkout\n"
" query and in the successful payment message, so put your order identifier\n"
" there.\n"
"\n"
" ## Regular currencies\n"
"\n"
" For physical goods, pass an ISO 4217 currency, a provider token from\n"
" [@BotFather](https://t.me/BotFather), and a price breakdown in the\n"
" smallest units of the currency:\n"
"\n"
" ```gleam\n"
" payments.invoice(\n"
" title: \"Order #42\",\n"
" description: \"2 pizzas\",\n"
" payload: \"order:42\",\n"
" currency: \"USD\",\n"
" provider_token: provider_token,\n"
" prices: [\n"
" payments.price(\"2x Pepperoni\", 2490),\n"
" payments.price(\"Delivery\", 500),\n"
" ],\n"
" )\n"
" |> payments.with_photo(\"https://example.com/pizza.jpg\")\n"
" |> payments.require_email()\n"
" |> payments.send(ctx)\n"
" ```\n"
"\n"
" Other builder options: `with_tips`, `with_flexible_shipping`,\n"
" `with_provider_data`, `with_start_parameter`, `with_reply_markup`,\n"
" `require_name`, `require_phone_number`. `create_link` builds a shareable\n"
" payment URL instead of sending a message.\n"
"\n"
" ## Answering the pre-checkout query\n"
"\n"
" After the user confirms the payment, Telegram sends a pre-checkout query\n"
" that **must be answered within 10 seconds**, otherwise the payment fails:\n"
"\n"
" ```gleam\n"
" import telega/router\n"
"\n"
" router.new(\"shop\")\n"
" |> router.on_pre_checkout_query(fn(ctx, query) {\n"
" let assert Ok(_) = case in_stock(query.invoice_payload) {\n"
" True -> payments.answer_pre_checkout_ok(ctx, query)\n"
" False -> payments.answer_pre_checkout_error(ctx, query, \"Out of stock\")\n"
" }\n"
" Ok(ctx)\n"
" })\n"
" ```\n"
"\n"
" ## Shipping queries\n"
"\n"
" For invoices created with `with_flexible_shipping`, Telegram asks the bot\n"
" for shipping options once the user fills in an address:\n"
"\n"
" ```gleam\n"
" router.on_shipping_query(router, fn(ctx, query) {\n"
" let assert Ok(_) = case ships_to(query.shipping_address) {\n"
" True ->\n"
" payments.answer_shipping_ok(ctx, query, [\n"
" payments.shipping_option(id: \"dhl\", title: \"DHL\", prices: [\n"
" payments.price(\"Shipping\", 500),\n"
" ]),\n"
" ])\n"
" False -> payments.answer_shipping_error(ctx, query, \"No delivery there\")\n"
" }\n"
" Ok(ctx)\n"
" })\n"
" ```\n"
"\n"
" ## Waiting for the payment in a conversation\n"
"\n"
" `wait_successful_payment` pauses the handler until the successful payment\n"
" service message arrives, so the whole purchase reads top-to-bottom (see\n"
" the [conversation guide](/docs/conversation.html)):\n"
"\n"
" ```gleam\n"
" fn buy_handler(ctx, _command) {\n"
" let assert Ok(_) =\n"
" payments.stars_invoice(\n"
" title: \"Premium\",\n"
" description: \"Premium access for a month\",\n"
" payload: \"premium:1m\",\n"
" amount: 100,\n"
" )\n"
" |> payments.send(ctx)\n"
"\n"
" use ctx, payment <- payments.wait_successful_payment(\n"
" ctx,\n"
" or: None,\n"
" timeout: Some(600_000),\n"
" )\n"
"\n"
" // Store payment.telegram_payment_charge_id — it is needed for refunds\n"
" reply.with_text(ctx, \"Thanks! Your order: \" <> payment.invoice_payload)\n"
" }\n"
" ```\n"
"\n"
" ## Refunds\n"
"\n"
" For Telegram Stars, refund through the raw API method with the charge id\n"
" from the successful payment:\n"
"\n"
" ```gleam\n"
" import telega/api\n"
" import telega/model/types\n"
"\n"
" api.refund_star_payment(\n"
" ctx.config.api_client,\n"
" parameters: types.RefundStarPaymentParameters(\n"
" user_id: user_id,\n"
" telegram_payment_charge_id: charge_id,\n"
" ),\n"
" )\n"
" ```\n"
).
-opaque invoice() :: {invoice,
binary(),
binary(),
binary(),
binary(),
list(telega@model@types:labeled_price()),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(integer()),
gleam@option:option(list(integer())),
gleam@option:option(boolean()),
gleam@option:option(boolean()),
gleam@option:option(boolean()),
gleam@option:option(boolean()),
gleam@option:option(boolean()),
gleam@option:option(telega@model@types:inline_keyboard_markup())}.
-file("src/telega/payments.gleam", 186).
?DOC(
" A labeled portion of the invoice price. `amount` is in the smallest units\n"
" of the currency (cents for USD, stars for XTR).\n"
).
-spec price(binary(), integer()) -> telega@model@types:labeled_price().
price(Label, Amount) ->
{labeled_price, Label, Amount}.
-file("src/telega/payments.gleam", 226).
-spec new_invoice(
binary(),
binary(),
binary(),
binary(),
gleam@option:option(binary()),
list(telega@model@types:labeled_price())
) -> invoice().
new_invoice(Title, Description, Payload, Currency, Provider_token, Prices) ->
{invoice,
Title,
Description,
Payload,
Currency,
Prices,
Provider_token,
none,
none,
none,
none,
none,
none,
none,
none,
none,
none,
none}.
-file("src/telega/payments.gleam", 191).
?DOC(" Invoice in Telegram Stars: no provider token, single price portion.\n").
-spec stars_invoice(binary(), binary(), binary(), integer()) -> invoice().
stars_invoice(Title, Description, Payload, Amount) ->
new_invoice(
Title,
Description,
Payload,
<<"XTR"/utf8>>,
none,
[price(Title, Amount)]
).
-file("src/telega/payments.gleam", 208).
?DOC(" Invoice in a regular currency through a payment provider.\n").
-spec invoice(
binary(),
binary(),
binary(),
binary(),
binary(),
list(telega@model@types:labeled_price())
) -> invoice().
invoice(Title, Description, Payload, Currency, Provider_token, Prices) ->
new_invoice(
Title,
Description,
Payload,
Currency,
{some, Provider_token},
Prices
).
-file("src/telega/payments.gleam", 256).
?DOC(" Product photo shown in the invoice.\n").
-spec with_photo(invoice(), binary()) -> invoice().
with_photo(Invoice, Url) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
{some, Url},
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 262).
?DOC(
" Allow tips up to `max` with suggested amounts (smallest currency units).\n"
" Not supported for Telegram Stars.\n"
).
-spec with_tips(invoice(), integer(), list(integer())) -> invoice().
with_tips(Invoice, Max, Suggested) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
{some, Max},
{some, Suggested},
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 277).
?DOC(
" Request a shipping address and make the final price depend on the chosen\n"
" shipping option — the bot will receive shipping queries, handle them with\n"
" `router.on_shipping_query` and `answer_shipping_ok`/`answer_shipping_error`.\n"
).
-spec with_flexible_shipping(invoice()) -> invoice().
with_flexible_shipping(Invoice) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
{some, true},
{some, true},
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 282).
?DOC(" JSON-serialized data for the payment provider.\n").
-spec with_provider_data(invoice(), binary()) -> invoice().
with_provider_data(Invoice, Data) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
{some, Data},
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 290).
?DOC(" Deep-linking parameter to recreate the invoice via a `/start` link.\n").
-spec with_start_parameter(invoice(), binary()) -> invoice().
with_start_parameter(Invoice, Parameter) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
{some, Parameter},
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 299).
?DOC(
" Inline keyboard for the invoice message. The first button must be a Pay\n"
" button, otherwise Telegram inserts one.\n"
).
-spec with_reply_markup(invoice(), telega@model@types:inline_keyboard_markup()) -> invoice().
with_reply_markup(Invoice, Markup) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
{some, Markup}}.
-file("src/telega/payments.gleam", 307).
?DOC(" Require the user's full name to complete the order.\n").
-spec require_name(invoice()) -> invoice().
require_name(Invoice) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
{some, true},
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 312).
?DOC(" Require the user's phone number to complete the order.\n").
-spec require_phone_number(invoice()) -> invoice().
require_phone_number(Invoice) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
{some, true},
erlang:element(15, Invoice),
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 317).
?DOC(" Require the user's email to complete the order.\n").
-spec require_email(invoice()) -> invoice().
require_email(Invoice) ->
{invoice,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(7, Invoice),
erlang:element(8, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(13, Invoice),
erlang:element(14, Invoice),
{some, true},
erlang:element(16, Invoice),
erlang:element(17, Invoice),
erlang:element(18, Invoice)}.
-file("src/telega/payments.gleam", 322).
?DOC(" Send the invoice to the current chat.\n").
-spec send(invoice(), telega@bot:context(any(), any(), any())) -> {ok,
telega@model@types:message()} |
{error, telega@error:telega_error()}.
send(Invoice, Ctx) ->
telega@api:send_invoice(
erlang:element(5, erlang:element(4, Ctx)),
{send_invoice_parameters,
{int, erlang:element(3, erlang:element(3, Ctx))},
none,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(7, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(9, Invoice),
erlang:element(10, Invoice),
erlang:element(8, Invoice),
none,
none,
none,
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
none,
none,
erlang:element(17, Invoice),
none,
none,
none,
none,
none,
erlang:element(18, Invoice)}
).
-file("src/telega/payments.gleam", 363).
?DOC(" Create a shareable payment link for the invoice.\n").
-spec create_link(invoice(), telega@bot:context(any(), any(), any())) -> {ok,
binary()} |
{error, telega@error:telega_error()}.
create_link(Invoice, Ctx) ->
telega@api:create_invoice_link(
erlang:element(5, erlang:element(4, Ctx)),
{create_invoice_link_parameters,
none,
erlang:element(2, Invoice),
erlang:element(3, Invoice),
erlang:element(4, Invoice),
erlang:element(7, Invoice),
erlang:element(5, Invoice),
erlang:element(6, Invoice),
none,
erlang:element(11, Invoice),
erlang:element(12, Invoice),
erlang:element(10, Invoice),
erlang:element(8, Invoice),
none,
none,
none,
erlang:element(13, Invoice),
erlang:element(14, Invoice),
erlang:element(15, Invoice),
erlang:element(16, Invoice),
none,
none,
erlang:element(17, Invoice)}
).
-file("src/telega/payments.gleam", 398).
?DOC(
" Confirm a pre-checkout query: the order can proceed.\n"
" Must be sent within 10 seconds after the query arrives.\n"
).
-spec answer_pre_checkout_ok(
telega@bot:context(any(), any(), any()),
telega@model@types:pre_checkout_query()
) -> {ok, boolean()} | {error, telega@error:telega_error()}.
answer_pre_checkout_ok(Ctx, Query) ->
telega@api:answer_pre_checkout_query(
erlang:element(5, erlang:element(4, Ctx)),
{answer_pre_checkout_query_parameters,
erlang:element(2, Query),
true,
none}
).
-file("src/telega/payments.gleam", 413).
?DOC(" Reject a pre-checkout query with a human-readable reason shown to the user.\n").
-spec answer_pre_checkout_error(
telega@bot:context(any(), any(), any()),
telega@model@types:pre_checkout_query(),
binary()
) -> {ok, boolean()} | {error, telega@error:telega_error()}.
answer_pre_checkout_error(Ctx, Query, Message) ->
telega@api:answer_pre_checkout_query(
erlang:element(5, erlang:element(4, Ctx)),
{answer_pre_checkout_query_parameters,
erlang:element(2, Query),
false,
{some, Message}}
).
-file("src/telega/payments.gleam", 429).
?DOC(" A shipping option offered in response to a shipping query.\n").
-spec shipping_option(
binary(),
binary(),
list(telega@model@types:labeled_price())
) -> telega@model@types:shipping_option().
shipping_option(Id, Title, Prices) ->
{shipping_option, Id, Title, Prices}.
-file("src/telega/payments.gleam", 438).
?DOC(" Confirm delivery to the queried address with the available options.\n").
-spec answer_shipping_ok(
telega@bot:context(any(), any(), any()),
telega@model@types:shipping_query(),
list(telega@model@types:shipping_option())
) -> {ok, boolean()} | {error, telega@error:telega_error()}.
answer_shipping_ok(Ctx, Query, Options) ->
telega@api:answer_shipping_query(
erlang:element(5, erlang:element(4, Ctx)),
{answer_shipping_query_parameters,
erlang:element(2, Query),
true,
{some, Options},
none}
).
-file("src/telega/payments.gleam", 455).
?DOC(" Reject a shipping query with a human-readable reason shown to the user.\n").
-spec answer_shipping_error(
telega@bot:context(any(), any(), any()),
telega@model@types:shipping_query(),
binary()
) -> {ok, boolean()} | {error, telega@error:telega_error()}.
answer_shipping_error(Ctx, Query, Message) ->
telega@api:answer_shipping_query(
erlang:element(5, erlang:element(4, Ctx)),
{answer_shipping_query_parameters,
erlang:element(2, Query),
false,
none,
{some, Message}}
).
-file("src/telega/payments.gleam", 482).
?DOC(
" Pauses the current chat actor's handler and waits for a successful payment\n"
" service message. Other (non-payment) messages keep the conversation\n"
" waiting, or go to the `or` handler if one is given.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = payments.stars_invoice(...) |> payments.send(ctx)\n"
" use ctx, payment <- payments.wait_successful_payment(ctx, or: None, timeout: None)\n"
" reply.with_text(ctx, \"Thanks! Charge id: \" <> payment.telegram_payment_charge_id)\n"
" ```\n"
"\n"
" See [conversation](/docs/conversation)\n"
).
-spec wait_successful_payment(
telega@bot:context(BNKX, BNKY, BNKZ),
gleam@option:option(telega@bot:handler(BNKX, BNKY, BNKZ)),
gleam@option:option(integer()),
fun((telega@bot:context(BNKX, BNKY, BNKZ), telega@model@types:successful_payment()) -> {ok,
telega@bot:context(BNKX, BNKY, BNKZ)} |
{error, BNKY})
) -> {ok, telega@bot:context(BNKX, BNKY, BNKZ)} | {error, BNKY}.
wait_successful_payment(Ctx, Handle_else, Timeout, Continue) ->
Payment_handler = {handle_message,
fun(Ctx@1, Message) -> case erlang:element(78, Message) of
{some, Payment} ->
Continue(Ctx@1, Payment);
none ->
wait_successful_payment(
Ctx@1,
Handle_else,
Timeout,
Continue
)
end end},
telega@bot:wait_handler(Ctx, Payment_handler, Handle_else, Timeout).