Skip to content

Commit

Permalink
Payments : implemented PreCheckoutQuery, AnswerPreCheckoutQuery,
Browse files Browse the repository at this point in the history
SuccessfulPayment. Test + docs + webhook. Docs Payments refactor
  • Loading branch information
MarioGattolla committed Jan 28, 2025
1 parent a6617fc commit 0c30d25
Show file tree
Hide file tree
Showing 22 changed files with 980 additions and 58 deletions.
31 changes: 23 additions & 8 deletions docs/12.features/9.dto.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ contains incoming data (a message or a callback query)
- `->id()` incoming _update_id_
- `->message()` (optional) an instance of [`Message`](#message)
- `->messageReaction()` (optional) an instance of [`Reaction`](#reaction)
- `->callbackQuery()` (optional) an instance of [`CallbackQuery`](#callback-query)
- `->callbackQuery()` (optional) an instance of [`CallbackQuery`](#callbackQuery)
- `->preCheckoutQuery()` (optional) an instance of [`PreCheckoutQuery`](#preCheckoutQuery)

## `Chat`

Expand Down Expand Up @@ -46,6 +47,7 @@ contains incoming data (a message or a callback query)
- `->venue()` (optional) an instance of [`Venue`](#venue) holding data about the contained sticker
- `->entities()` (optional) a collection of [`Entity`](#entity) holding data about the contained entity
- `->invoice()` (optional) an instance of [`Invoice`](#invoice) holding data about the contained invoice
- `->successfulPayment()` (optional) an instance of [`SuccessfulPayment`](#successfulPayment) holding data about the successful payment with information about the payment
- `->newChatMembers()` a collection of [`User`](#user) holding the list of users that joined the group/supergroup
- `->leftChatMember()` (optional) an instance of [`User`](#user) holding data about the user that left the group/supergroup
- `->webAppData()` (optional) incoming data from sendData method of telegram WebApp
Expand All @@ -57,10 +59,14 @@ contains incoming data (a message or a callback query)
## `CallbackQuery`

- `->id()` incoming _callback_query_id_
- `->from()` (optional) an instance of the [`User`](#user) that triggered the callback query
- `->from()` an instance of the [`User`](#user) that triggered the callback query
- `->message()` (optional) an instance of the [`Message`](#message) that triggered the callback query
- `->data()` an `Illuminate\Support\Collection` that holds the key/value pairs of the callback query data

## `PreCheckoutQuery`

ore information on the [payment](payment#preCheckoutQuery) page.

## `Reaction`

- `->id()` incoming _message_id_
Expand Down Expand Up @@ -90,11 +96,8 @@ contains incoming data (a message or a callback query)

## `Invoice`

- `->title()` invoice title
- `->description()` invoice description
- `->startParameter()` unique bot deep-linking parameter that can be used to generate this invoice
- `->currency()` invoice currency
- `->totalAmount()` invoice total amount (integer, not float/double)
More information on the [payment](payment#invoice) page.


## `Audio`

Expand Down Expand Up @@ -283,4 +286,16 @@ represents a join request sent to a chat.
- `->userChatId()` identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier. The bot can use this identifier for 5 minutes to send messages until the join request is processed, assuming no other administrator contacted the user.
- `->date()` date the request was sent in Unix time
- `->bio()` (optional) bio of the user
- `->inviteLink()` (optional) an instance of [`ChatInviteLink`](#chat-invite-link) that was used by the user to send the join request
- `->inviteLink()` (optional) an instance of [`ChatInviteLink`](#chatInviteLink) that was used by the user to send the join request

## `OrderInfo`

represents information about an order. More information on the [payment](payment#orderInfo) page.

## `ShippingAddress`

represents a shipping address. More information on the [payment](payment#shippingAddress) page.

## `SuccessfulPayment`

represents a successful payment. More information on the [payment](payment#successfulPayment) page.
171 changes: 171 additions & 0 deletions docs/12.features/payment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: 'Payments'
---

## How it works

After creating a Bot you need to have a payment provider token, see https://core.telegram.org/bots/payments for more information about it.

Invoices are sent via the [`Invoice`](#invoice) method. The bot forms an invoice message with a description of the goods or service,
amount to be paid, and requested shipping info.

Once the payment is completed the Bot API sends an Update with the field [`preCheckoutQuery`](#precheckoutquery) to the bot that
contains all the available information about the order. Your bot must reply using answerPrecheckoutQuery through the Webhook Handler within
10 seconds after receiving this update or the transaction is canceled. Webhook Handler is already set to send it through the ```handlePreCheckoutQuery()``` method.

The bot may return an error if it can't process the order for any reason. We highly recommend specifying a reason for
failure to complete the order in human readable form (e.g. "Sorry, we're all out of rubber ducks! Would you be interested
in a cast iron bear instead?"). Telegram will display this reason to the user.

In case the bot confirms the order, Telegram requests the payment provider to complete the transaction.
If the payment information was entered correctly and the payment goes through, the API will send a receipt
message of the type ```successful_payment``` through the Webhook Handler method ```handleSuccessfulPayment()```.

See the [`example`](#example) below for further clarification


## Example

Example : System for creating payments and saving invoices

Let's create a new class that extends the ```WebhookHandler``` class

```php
class TestInvoicingHandler extends WebhookHandler
{

}
```

Let's create a button for the item for sale

```php
public function exampleButton(): void
{
$this->chat->message('Table')
->keyboard(fn(Keyboard $keyboard) => $keyboard->button('Buy')->action('buy')->param('item_id', 42));
}
```

We need to create a function that sends the invoice and , if necessary, stores it

```php
public function buy(int $item_id): void
{
$invoice = InvoiceModel::create([...]);

$this->chat->invoice('Attached is the invoice for your order')
->currency('EUR')
->addItem('Table', 100)
->payload($invoice->id)
->invoice();
}
```

Once the payment is completed, we can use the Webhook Handler for further operations

```php
protected function handleSuccessfulPayment(SuccessfulPayment $successfulPayment): void
{
//Example : check the invoice data
$invoice = Invoice::findOrFail($successfulPayment->invoicePayload());

if ($invoice->total() !== $successfulPayment->totalAmount()) {
//...errors
}
}
```


### Attachments

Invoices can be sent through Telegraph `->invoice()` method:

```php
Telegraph::invoice('Invoice title')
->description('Invoice Description')
->currency('EUR') //Pass “XTR” for payments in Telegram Stars
->addItem('First Item Label', 10) //Must contain exactly one item for payments in Telegram Stars
->addItem('Second Item Label', 10)
->maxTip(70) //Not supported for payments in Telegram Stars
->suggestedTips([30,20])
->startParameter(10)
->image('Invoice Image Link', 20 , 20)
->needName() //Ignored for payments in Telegram Stars
->needPhoneNumber() //Ignored for payments in Telegram Stars
->needEmail() //Ignored for payments in Telegram Stars
->needShippingAddress() //Ignored for payments in Telegram Stars
->flexible() //Ignored for payments in Telegram Stars
->send();
```

### Incoming Data

## `Invoice`

- `->title()` invoice title
- `->description()` invoice description
- `->startParameter()` unique bot deep-linking parameter that can be used to generate this invoice
- `->currency()` invoice currency
- `->totalAmount()` invoice total amount (integer, not float/double)


## `PreCheckoutQuery`

- `->id()` unique query identifier
- `->from()` an instance of the [dto](9.dto.md#user) that triggered the query
- `->currency()` three-letter ISO 4217 currency code, or “XTR” for payments in Telegram Stars
- `->totalAmount()` total price in the smallest units of the currency
- `->invoicePayload()` bot-specified invoice payload
- `->ShippingOptionId()` (optional) identifier of the shipping option chosen by the user
- `->orderInfo()` (optional) an instance of the [`OrderInfo`](#orderinfo) order information provided by the user

## `OrderInfo`

represents information about an order.

- `->name()` (optional) user name
- `->phoneNumber()` (optional) user's phone number
- `->email()` (optional) user email
- `->shippingAddress()` (optional) an instance of [`ShippingAddress`](#shippingAddress) user shipping address


## `ShippingAddress`

represents a shipping address.

- `->countryCode()` two-letter ISO 3166-1 alpha-2 country code
- `->state()` state, if applicable
- `->city()` city
- `->streetLine1()` first line for the address
- `->streetLine2()` second line for the address
- `->postCode()` address post code

## `PreCheckoutQuery`

- `->id()` unique query identifier
- `->from()` an instance of the [dto](9.dto.md#user) that triggered the query
- `->currency()` three-letter ISO 4217 currency code, or “XTR” for payments in Telegram Stars
- `->totalAmount()` total price in the smallest units of the currency
- `->invoicePayload()` bot-specified invoice payload
- `->ShippingOptionId()` (optional) identifier of the shipping option chosen by the user
- `->orderInfo()` (optional) an instance of the [`OrderInfo`](#orderinfo) order information provided by the user


## `SuccessfulPayment`

represents a successful payment.

- `->currency()` three-letter ISO 4217 currency code, or “XTR” for payments in Telegram Stars
- `->totalAmount()` total price in the smallest units of the currency
- `->invoicePayload()` bot-specified invoice payload
- `->subscriptionExpirationDate()` (optional) expiration date of the subscription, in Unix time; for recurring payments only
- `->isRecurring()` (optional) true, if the payment is a recurring payment for a subscription
- `->isFirstRecurring()` (optional) true, if the payment is the first payment for a subscription
- `->shippingOptionId()` (optional) identifier of the shipping option chosen by the user
- `->orderInfo()` (optional) order information provided by the user
- `->telegramPaymentChargeId()` telegram payment identifier
- `->providerPaymentChargeId()` provider payment identifier

> [!NOTE]
> If the buyer initiates a chargeback with the relevant payment provider following this transaction, the funds may be debited from your balance. This is outside of Telegram's control.
32 changes: 1 addition & 31 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,31 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" executionOrder="random" failOnWarning="true" failOnRisky="true" failOnEmptyTestSuite="true" beStrictAboutOutputDuringTests="true" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="DefStudio Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<report>
<html outputDirectory="build/coverage"/>
<text outputFile="build/coverage.txt"/>
<clover outputFile="build/logs/clover.xml"/>
</report>
</coverage>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<server name="SANDOBOX_TELEGRAM_BOT_TOKEN" value=":fake_bot_token:"/>
<server name="SANDBOX_TELEGRAM_CHAT_ID" value=""/>
<server name="SANDOBOX_TELEGRAM_PAYMENT_PROVIDER_TOKEN" value=""/>
</php>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
<exclude>
<directory>./src/Exceptions</directory>
</exclude>
</source>
</phpunit>
<?php
14 changes: 14 additions & 0 deletions src/Concerns/InteractsWithWebhooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,18 @@ public function replyWebhook(int $callbackQueryId, string $message, bool $showAl

return $telegraph;
}

public function answerPreCheckoutQuery(int $preCheckoutQueryId, bool $result, ?string $errorMessage = null): Telegraph
{
$telegraph = clone $this;

$telegraph->endpoint = self::ENDPOINT_ANSWER_PRE_CHECKOUT_QUERY;
$telegraph->data = [
'pre_checkout_query_id' => $preCheckoutQueryId,
'ok' => $result,
'error_message' => $errorMessage,
];

return $telegraph;
}
}
20 changes: 13 additions & 7 deletions src/DTO/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,16 @@ class Message implements Arrayable
{
private int $id;
private ?int $messageThreadId = null;

private CarbonInterface $date;
private ?CarbonInterface $editDate = null;

private string $text;
/** Can be string or json string. if json then convert it to array */
private mixed $webAppData = null;
private bool $protected = false;

private ?User $from = null;
private ?User $forwardedFrom = null;

private ?Chat $chat = null;
private Keyboard $keyboard;

private ?Message $replyToMessage = null;

/** @var Collection<array-key, User> */
Expand All @@ -51,9 +46,8 @@ class Message implements Arrayable
private ?Voice $voice = null;
private ?Sticker $sticker = null;
private ?Venue $venue = null;

private ?Invoice $invoice = null;

private ?SuccessfulPayment $successfulPayment = null;
private ?WriteAccessAllowed $writeAccessAllowed = null;

/** @var Collection<array-key, Entity> */
Expand Down Expand Up @@ -90,6 +84,7 @@ private function __construct()
* venue?: array<string, mixed>,
* contact?: array<string, mixed>,
* invoice?: array<string, mixed>,
* successful_payment?: array<string, mixed>,
* new_chat_members?: array<string, mixed>,
* left_chat_member?: array<string, mixed>,
* web_app_data?: array<string, mixed>,
Expand Down Expand Up @@ -198,6 +193,11 @@ public static function fromArray(array $data): Message
$message->invoice = Invoice::fromArray($data['invoice']);
}

if (isset($data['successful_payment'])) {
/* @phpstan-ignore-next-line */
$message->successfulPayment = SuccessfulPayment::fromArray($data['successful_payment']);
}

/* @phpstan-ignore-next-line */
$message->newChatMembers = collect($data['new_chat_members'] ?? [])->map(fn (array $userData) => User::fromArray($userData));

Expand Down Expand Up @@ -343,6 +343,11 @@ public function invoice(): ?Invoice
return $this->invoice;
}

public function successfulPayment(): ?SuccessfulPayment
{
return $this->successfulPayment;
}

/**
* @return Collection<array-key, User>
*/
Expand Down Expand Up @@ -399,6 +404,7 @@ public function toArray(): array
'sticker' => $this->sticker?->toArray(),
'venue' => $this->venue?->toArray(),
'invoice' => $this->invoice?->toArray(),
'successful_payment' => $this->successfulPayment?->toArray(),
'new_chat_members' => $this->newChatMembers->toArray(),
'left_chat_member' => $this->leftChatMember,
'web_app_data' => $this->webAppData,
Expand Down
Loading

0 comments on commit 0c30d25

Please sign in to comment.