Compare commits

...

16 Commits
3.x ... master

Author SHA1 Message Date
ndeet
a3004b382b
Adding new subscription routes and updating subs examples. (#142)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
* Adding new subscription routes and updating subs examples.

* Updating github actions due to deprecated nodejs 20.
2026-04-30 11:29:35 +02:00
ndeet
60e6be57f9
Fixes #136 by adding BTCPay >=2.0 parameter. (#141)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
2026-01-21 12:31:48 +01:00
Števo Bartko
7d244da111
FIX - php8.4 compatiblity - Explicit nullable param type (#133) 2026-01-21 11:47:07 +01:00
ndeet
e9b01b5266
Change hash comparison, fixes #138 (#140) 2026-01-20 22:51:56 +01:00
ndeet
db79fee3d2
Add subscriptions support. (#139)
* Add subscriptions support.
2026-01-20 22:43:02 +01:00
ndeet
3118f9e4e0
BTCPay 2.0 compatibilty for pull payments due to changed parameter name. (#132)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
2024-11-22 17:34:09 +01:00
ndeet
f4fac20f19
Adding filters to invoices search. (#130)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
* Adding filters to invoices search.
2024-11-05 21:44:23 +01:00
ndeet
28197bf65f
Merge pull request #128 from ndeet/cleanup
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
Cleanup and adding getCurrency() for new data field.
2024-09-16 23:02:29 +02:00
ndeet
108f18b444 Cleanup and adding getCurrency() for new data field. 2024-09-16 23:01:08 +02:00
ndeet
5e2ba7e3f5 Move description parameter to end to not cause breaking change in minor release.
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
2024-09-13 16:54:13 +02:00
ndeet
9bbb7e8ebe
BTCPay 2.0 compatibiltiy fixes in a backward compatible way. (#127)
* BTCPay 2.0 compatibiltiy fixes in a backward compatible way.

* Update GH workflow dependencies.
2024-09-12 16:54:24 +02:00
Markus Petzsch
60e7b42de2
add description field to pull payment (#125)
* add description field to pull payments
2024-06-27 15:04:02 +02:00
ndeet
c115b04157
Add refund invoice endpoint. Add getters to ApiKey result. (#123)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
PHP Unit Tests / phpunit (8.0, latest) (push) Has been cancelled
PHP Unit Tests / phpunit (8.1, latest) (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
* Add refund invoice endpoint. 
* Add getters to ApiKey result.
2024-04-25 11:19:49 +02:00
ndeet
385b7f6882
Adding missing stores endpoints update/delete (#122)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
PHP Unit Tests / phpunit (8.0, latest) (push) Has been cancelled
PHP Unit Tests / phpunit (8.1, latest) (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
* Adding missing functions of updating and deleting store; renaming examples file.
2024-04-03 10:35:46 +02:00
ndeet
b88cd1cf5c
Create onchain wallet (#120)
Some checks failed
Code Style / php-cs-fixer (push) Has been cancelled
PHP Unit Tests / phpunit (8.0, latest) (push) Has been cancelled
PHP Unit Tests / phpunit (8.1, latest) (push) Has been cancelled
Static analysis (Psalm) / Psalm (8.0) (push) Has been cancelled
* Adding createStoreOnChainWallet functionality.
2024-03-25 19:42:58 +01:00
ndeet
8b119a836a
Fix http status check on create user. (#113) 2023-06-28 22:45:39 +02:00
37 changed files with 1724 additions and 98 deletions

View File

@ -8,7 +8,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Run PHP CS Fixer
uses: docker://oskarstark/php-cs-fixer-ga

View File

@ -1,53 +1,53 @@
name: PHP Unit Tests
env:
BTCPAY_HOST: ${{ secrets.BTCPAY_HOST }}
BTCPAY_API_KEY: ${{ secrets.BTCPAY_API_KEY }}
BTCPAY_STORE_ID: ${{ secrets.BTCPAY_STORE_ID }}
BTCPAY_NODE_URI: ${{ secrets.BTCPAY_NODE_URI }}
on: [ push, pull_request ]
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['8.0', '8.1']
phpunit-versions: ['latest']
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: '0'
- name: Setup PHP, with composer and extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: composer:v2, phpunit:${{ matrix.phpunit-versions }}
extensions: curl, json, mbstring, bcmath
coverage: xdebug #optional
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: composer install --no-progress --optimize-autoloader
- name: Test with phpunit
run: vendor/bin/phpunit --coverage-text
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
#name: PHP Unit Tests
#env:
# BTCPAY_HOST: ${{ secrets.BTCPAY_HOST }}
# BTCPAY_API_KEY: ${{ secrets.BTCPAY_API_KEY }}
# BTCPAY_STORE_ID: ${{ secrets.BTCPAY_STORE_ID }}
# BTCPAY_NODE_URI: ${{ secrets.BTCPAY_NODE_URI }}
#on: [ push, pull_request ]
#
#jobs:
# phpunit:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# php-versions: ['8.0', '8.1']
# phpunit-versions: ['latest']
#
#
# steps:
# - name: Checkout
# uses: actions/checkout@v3
# with:
# fetch-depth: '0'
#
# - name: Setup PHP, with composer and extensions
# uses: shivammathur/setup-php@v2
# with:
# php-version: ${{ matrix.php-versions }}
# tools: composer:v2, phpunit:${{ matrix.phpunit-versions }}
# extensions: curl, json, mbstring, bcmath
# coverage: xdebug #optional
#
# - name: Get composer cache directory
# id: composer-cache
# run: echo "::set-output name=dir::$(composer config cache-files-dir)"
#
# - name: Cache composer dependencies
# uses: actions/cache@v2
# with:
# path: ${{ steps.composer-cache.outputs.dir }}
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
# restore-keys: ${{ runner.os }}-composer-
#
# - name: Install Composer dependencies
# run: composer install --no-progress --optimize-autoloader
#
# - name: Test with phpunit
# run: vendor/bin/phpunit --coverage-text
#
# - name: Setup problem matchers for PHP
# run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
#
# - name: Setup problem matchers for PHPUnit
# run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

View File

@ -12,7 +12,7 @@ jobs:
php-versions: ['8.0']
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
fetch-depth: '0'
@ -27,7 +27,7 @@ jobs:
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
.php-cs-fixer.cache
*.cache
composer.lock
/tests/.env
/tests/.env
/.claude/

View File

@ -22,3 +22,22 @@ try {
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
// Get 2 invoices, skip 2
try {
echo 'Get invoices:' . PHP_EOL;
$client = new Invoice($host, $apiKey);
var_dump($client->getAllInvoices($storeId, 2, 2));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
// Get newer/equal than 2024-10-20
try {
echo 'Get invoices newer/equal than 2024-10-20:' . PHP_EOL;
$date = new DateTime('2024-10-20');
$client = new Invoice($host, $apiKey);
var_dump($client->getAllInvoicesWithFilter($storeId, null, null, null, $date->getTimestamp()));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}

View File

@ -43,7 +43,7 @@ class PullPayments
$autoApproveClaims = false;
$startsAt = null;
$expiresAt = null;
$paymentMethods = ['BTC'];
$paymentMethods = ['BTC-CHAIN'];
try {
$client = new PullPayment($this->host, $this->apiKey);
@ -113,7 +113,7 @@ class PullPayments
public function approvePayout()
{
$payoutId ='';
$payoutId = '';
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->approvePayout(
@ -163,7 +163,7 @@ class PullPayments
$pullPaymentId = '';
$destination = '';
$amount = PreciseNumber::parseString('0.000001');
$paymentMethod = '';
$paymentMethod = 'BTC-CHAIN';
try {
$client = new PullPayment($this->host, $this->apiKey);

View File

@ -32,6 +32,21 @@ class StoreOnChainWallets
}
}
public function createStoreOnChainWallet()
{
$cryptoCode = 'BTC';
try {
$client = new StoreOnChainWallet($this->host, $this->apiKey);
var_dump($client->createStoreOnchainWallet(
$this->storeId,
$cryptoCode
));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
}
public function getStoreOnChainWalletFeeRate()
{
$cryptoCode = 'BTC';
@ -171,3 +186,4 @@ $store = new StoreOnChainWallets();
$store->getStoreOnChainWalletTransactions();
//$store->getStoreOnChainWalletTransaction();
//$store->getStoreOnChainWalletUTXOs();
//$store->createStoreOnChainWallet();

View File

@ -8,9 +8,10 @@ use BTCPayServer\Client\Store;
$apiKey = '';
$host = ''; // e.g. https://your.btcpay-server.tld
$storeId = '';
$invoiceId = '';
$updateStoreId = '';
// Get information about store on BTCPay Server.
try {
$client = new Store($host, $apiKey);
var_dump($client->getStore($storeId));
@ -21,7 +22,17 @@ try {
// Create a new store.
try {
$client = new Store($host, $apiKey);
var_dump($client->createStore('my new store'));
$newStore = $client->createStore('New store', null, 'EUR');
var_dump($newStore);
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
// Update a store.
// You need to pass all variables to make sure it does not get reset to defaults if you want to preserve them.
try {
$client = new Store($host, $apiKey);
var_dump($client->updateStore($updateStoreId, 'Store name CHANGED'));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}

242
examples/subscriptions.php Normal file
View File

@ -0,0 +1,242 @@
<?php
// Include autoload file.
require __DIR__ . '/../vendor/autoload.php';
// Import Subscriptions client class.
use BTCPayServer\Client\Subscriptions;
// Fill in with your BTCPay Server data.
$apiKey = '';
$host = ''; // e.g. https://your.btcpay-server.tld
$storeId = '';
// Create the subscriptions client.
try {
$client = new Subscriptions($host, $apiKey);
echo "=== BTCPay Server Subscriptions API Examples ===\n\n";
// 1. Create a new offering
echo "1. Creating a new offering...\n";
$offering = $client->createOffering(
$storeId,
'Premium SaaS App',
'https://example.com/success',
[
'category' => 'saas',
'region' => 'us',
'version' => '1.0'
],
[
['id' => 'feature-analytics', 'description' => 'Advanced analytics dashboard'],
['id' => 'feature-support', 'description' => '24/7 priority support'],
['id' => 'feature-api', 'description' => 'Unlimited API access']
]
);
echo "Offering created with ID: " . $offering->getId() . "\n";
echo "App Name: " . $offering->getAppName() . "\n\n";
$offeringId = $offering->getId();
// 2. Create plans for the offering
echo "2. Creating plans for the offering...\n";
// Basic plan
$basicPlan = $client->createOfferingPlan(
$storeId,
$offeringId,
'Basic monthly subscription with essential features',
'USD',
7,
'Basic Plan',
true,
'1.99',
true,
null,
['tier' => 'basic'],
'Monthly',
['feature-analytics']
);
echo "Basic plan created with ID: " . $basicPlan->getId() . "\n";
// Premium plan
$premiumPlan = $client->createOfferingPlan(
$storeId,
$offeringId,
'Premium monthly subscription with all features',
'USD',
7,
'Premium Plan',
true,
'29.99',
true,
14,
['tier' => 'premium'],
'Monthly',
['feature-analytics', 'feature-support', 'feature-api']
);
echo "Premium plan created with ID: " . $premiumPlan->getId() . "\n\n";
$basicPlanId = $basicPlan->getId();
$premiumPlanId = $premiumPlan->getId();
// 2b. Update the offering
echo "2b. Updating the offering...\n";
$updatedOffering = $client->updateOffering(
$storeId,
$offeringId,
'Premium SaaS App v2',
'https://example.com/success-v2',
['category' => 'saas', 'region' => 'eu', 'version' => '2.0']
);
echo "Offering updated: " . $updatedOffering->getAppName() . "\n\n";
// 2c. Update a plan
echo "2c. Updating the basic plan...\n";
$updatedPlan = $client->updateOfferingPlan(
$storeId,
$offeringId,
$basicPlanId,
'Updated basic monthly subscription',
'USD',
7,
'Basic Plan v2',
null,
'2.99'
);
echo "Plan updated: " . $updatedPlan->getName() . " - " . $updatedPlan->getPrice() . " " . $updatedPlan->getCurrency() . "\n\n";
// 3. Get all offerings for the store
echo "3. Getting all offerings for the store...\n";
$offerings = $client->getOfferings($storeId);
foreach ($offerings->all() as $off) {
echo "- Offering: " . $off->getAppName() . " (ID: " . $off->getId() . ")\n";
foreach ($off->getPlans() as $plan) {
echo " - Plan: " . $plan->getName() . " - " . $plan->getPrice() . " " . $plan->getCurrency() . "/" . $plan->getRecurringType() . "\n";
}
}
echo "\n";
// 4. Get a specific offering
echo "4. Getting specific offering details...\n";
$specificOffering = $client->getOffering($storeId, $offeringId);
echo "Offering: " . $specificOffering->getAppName() . "\n";
echo "Success URL: " . $specificOffering->getSuccessRedirectUrl() . "\n";
echo "Number of plans: " . count($specificOffering->getPlans()) . "\n\n";
// 5. Get a specific plan
echo "5. Getting specific plan details...\n";
$specificPlan = $client->getOfferingPlan($storeId, $offeringId, $basicPlanId);
echo "Plan: " . $specificPlan->getName() . "\n";
echo "Price: " . $specificPlan->getPrice() . " " . $specificPlan->getCurrency() . "\n";
echo "Trial Days: " . $specificPlan->getTrialDays() . "\n";
echo "Features: " . implode(', ', $specificPlan->getFeatures()) . "\n\n";
// 6. Create a plan checkout session
echo "6. Creating a plan checkout session...\n";
$checkout = $client->createPlanCheckout(
$storeId,
$offeringId,
$basicPlanId,
null, // If the customer already exists on BTCPay, fill the email or other id here.
60,
'SoftMigration',
['source' => 'web'],
['campaign' => 'summer2026'],
['flow' => 'new_signup'],
false,
null, // You can override the plan price here if you want to force more credit or custom amount.
'https://example.com/welcome',
'test@example.com' // This is optional and will prefill the checkout page with the email.
);
echo "Checkout created with ID: " . $checkout->getId() . "\n";
echo "Checkout URL: " . $checkout->getUrl() . "\n";
echo "Is Trial: " . ($checkout->isTrial() ? 'Yes' : 'No') . "\n";
echo "New Subscriber: " . ($checkout->isNewSubscriber() ? 'Yes' : 'No') . "\n\n";
$checkoutId = $checkout->getId();
// 7. Get plan checkout details
echo "7. Getting plan checkout details...\n";
$checkoutDetails = $client->getPlanCheckout($checkoutId);
echo "Checkout ID: " . $checkoutDetails->getId() . "\n";
echo "Plan: " . $checkoutDetails->getPlan()->getName() . "\n";
$subscriber = $checkoutDetails->getSubscriber();
if ($subscriber && $subscriber->getCustomer()->getIdentities()) {
echo "Subscriber Email: " . ($subscriber->getCustomer()->getIdentities()['Email'] ?? 'N/A') . "\n";
}
echo "\n";
// 8. Subscriber management examples
/*
// Fill these variables with actual values to test subscriber operations
$offeringId = ''; // e.g. "offering_GFbMSBpybM6i5uEiqc"
$customerSelector = ''; // e.g. "ps_N71XxcPDnKNgNDxKHZ" or customer email
$suspensionReason = 'User requested cancellation';
if (!empty($storeId) && !empty($offeringId) && !empty($customerSelector)) {
try {
// Get subscriber details
echo "8. Getting subscriber details...\n";
$subscriber = $client->getSubscriber($storeId, $offeringId, $customerSelector);
echo "Customer ID: " . $subscriber->getCustomer()->getId() . "\n";
echo "Active: " . ($subscriber->isActive() ? 'Yes' : 'No') . "\n";
echo "Phase: " . $subscriber->getPhase() . "\n";
echo "Created: " . date('Y-m-d H:i:s', $subscriber->getCreated()) . "\n";
echo "\n";
// Suspend subscriber
if (!empty($suspensionReason)) {
echo "9. Suspending subscriber...\n";
$client->suspendSubscriber($storeId, $offeringId, $customerSelector, $suspensionReason);
echo "Subscriber suspended successfully!\n";
// Check status after suspension
$suspendedSubscriber = $client->getSubscriber($storeId, $offeringId, $customerSelector);
echo "Status after suspension: " . ($suspendedSubscriber->isActive() ? 'Active' : 'Suspended') . "\n";
echo "Suspension reason: " . ($suspendedSubscriber->getSuspensionReason() ?? 'N/A') . "\n\n";
// Update subscriber dates
echo "9b. Updating subscriber dates...\n";
$updatedSubscriber = $client->updateSubscriberDates(
$storeId,
$offeringId,
$customerSelector,
null,
time() + (30 * 24 * 60 * 60) // 30 days from now
);
echo "Subscriber expiration updated\n";
echo "Scheduled plan: " . ($updatedSubscriber->getScheduledPlan() ? $updatedSubscriber->getScheduledPlan()->getName() : 'None') . "\n";
echo "Scheduled plan activates at: " . ($updatedSubscriber->getScheduledPlanActivatesAt() ? date('Y-m-d H:i:s', $updatedSubscriber->getScheduledPlanActivatesAt()) : 'N/A') . "\n\n";
// Unsuspend subscriber
echo "10. Unsuspending subscriber...\n";
$client->unsuspendSubscriber($storeId, $offeringId, $customerSelector);
echo "Subscriber unsuspended successfully!\n";
// Check status after unsuspending
$reactivatedSubscriber = $client->getSubscriber($storeId, $offeringId, $customerSelector);
echo "Status after unsuspending: " . ($reactivatedSubscriber->isActive() ? 'Active' : 'Suspended') . "\n";
echo "Suspension reason: " . ($reactivatedSubscriber->getSuspensionReason() ?? 'N/A') . "\n\n";
}
// Delete subscriber
echo "11. Deleting subscriber...\n";
$client->deleteSubscriber($storeId, $offeringId, $customerSelector);
echo "Subscriber deleted successfully!\n\n";
} catch (\Throwable $e) {
echo "Error in subscriber management: " . $e->getMessage() . "\n";
}
} else {
echo "8. Subscriber management examples skipped - please fill in storeId, offeringId, and customerSelector variables\n";
}
*/
echo "=== Examples completed successfully! ===\n";
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage() . "\n";
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
}

View File

@ -22,7 +22,7 @@ class AbstractClient
/** @var ClientInterface */
private $httpClient;
public function __construct(string $baseUrl, string $apiKey, ClientInterface $client = null)
public function __construct(string $baseUrl, string $apiKey, ?ClientInterface $client = null)
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->apiKey = $apiKey;

View File

@ -7,6 +7,7 @@ namespace BTCPayServer\Client;
use BTCPayServer\Result\Invoice as ResultInvoice;
use BTCPayServer\Result\InvoiceList;
use BTCPayServer\Result\InvoicePaymentMethod;
use BTCPayServer\Result\PullPayment as ResultPullPayment;
use BTCPayServer\Util\PreciseNumber;
class Invoice extends AbstractClient
@ -115,19 +116,71 @@ class Invoice extends AbstractClient
}
}
public function getAllInvoices(string $storeId): InvoiceList
{
return $this->_getAllInvoicesWithFilter($storeId, null);
}
public function getInvoicesByOrderIds(string $storeId, array $orderIds): InvoiceList
{
return $this->_getAllInvoicesWithFilter($storeId, $orderIds);
}
private function _getAllInvoicesWithFilter(
public function getAllInvoices(
string $storeId,
array $filterByOrderIds = null
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, null, null, null, null, null, $take, $skip);
}
public function getInvoicesByOrderIds(
string $storeId,
array $orderIds,
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, $orderIds, null, null, null, null, $take, $skip);
}
public function getInvoicesByText(
string $storeId,
string $text,
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, null, $text, null, null, null, $take, $skip);
}
public function getInvoicesByStatus(
string $storeId,
array $status,
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, null, null, $status, null, null, $take, $skip);
}
public function getInvoicesByStartDate(
string $storeId,
int $startDate,
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, null, null, null, $startDate, null, $take, $skip);
}
public function getInvoicesByEndDate(
string $storeId,
int $endDate,
?int $take = null,
?int $skip = null
): InvoiceList {
return $this->getAllInvoicesWithFilter($storeId, null, null, null, null, $endDate, $take, $skip);
}
/**
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#operation/Invoices_GetInvoices
*/
public function getAllInvoicesWithFilter(
string $storeId,
?array $filterByOrderIds = null,
?string $filterByText = null,
?array $filterByStatus = null,
?int $filterByStartDate = null,
?int $filterByEndDate = null,
?int $take = null,
?int $skip = null
): InvoiceList {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/invoices?';
if ($filterByOrderIds !== null) {
@ -135,6 +188,26 @@ class Invoice extends AbstractClient
$url .= 'orderId=' . urlencode($filterByOrderId) . '&';
}
}
if ($filterByText !== null) {
$url .= 'textSearch=' . urlencode($filterByText) . '&';
}
if ($filterByStatus !== null) {
foreach ($filterByStatus as $filterByStatusItem) {
$url .= 'status=' . urlencode($filterByStatusItem) . '&';
}
}
if ($filterByStartDate !== null) {
$url .= 'startDate=' . $filterByStartDate . '&';
}
if ($filterByEndDate !== null) {
$url .= 'endDate=' . $filterByEndDate . '&';
}
if ($take !== null) {
$url .= 'take=' . $take . '&';
}
if ($skip !== null) {
$url .= 'skip=' . $skip . '&';
}
// Clean URL.
$url = rtrim($url, '&');
@ -159,7 +232,8 @@ class Invoice extends AbstractClient
public function getPaymentMethods(string $storeId, string $invoiceId): array
{
$method = 'GET';
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/invoices/' . urlencode($invoiceId) . '/payment-methods';
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/invoices/'
. urlencode($invoiceId) . '/payment-methods';
$headers = $this->getRequestHeaders();
$response = $this->getHttpClient()->request($method, $url, $headers);
@ -181,11 +255,15 @@ class Invoice extends AbstractClient
}
}
/**
* Mark an invoice status.
*
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#operation/Invoices_MarkInvoiceStatus
* @throws \JsonException
*/
public function markInvoiceStatus(string $storeId, string $invoiceId, string $markAs): ResultInvoice
{
$url = $this->getApiUrl() . 'stores/' . urlencode(
$storeId
) . '/invoices/' . urlencode($invoiceId) . '/status';
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/invoices/' . urlencode($invoiceId) . '/status';
$headers = $this->getRequestHeaders();
$method = 'POST';
@ -206,4 +284,49 @@ class Invoice extends AbstractClient
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
/**
* Refund an invoice.
*
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#operation/Invoices_Refund
* @throws \JsonException
*/
public function refundInvoice(
string $storeId,
string $invoiceId,
?string $refundVariant = 'CurrentRate',
?string $paymentMethod = 'BTC',
?string $name = null,
?string $description = null,
?float $subtractPercentage = 0.0,
?PreciseNumber $customAmount = null,
?string $customCurrency = null
): ResultPullPayment {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/invoices/' . urlencode($invoiceId) . '/refund';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'name' => $name,
'description' => $description,
'paymentMethod' => $paymentMethod,
'refundVariant' => $refundVariant,
'subtractPercentage' => $subtractPercentage,
'customAmount' => $customAmount?->__toString(),
'customCurrency' => $customCurrency
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new ResultPullPayment(
json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR)
);
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
}

View File

@ -170,7 +170,7 @@ class InvoiceCheckoutOptions
$lastIndex = strrpos($k, $separator);
if ($lastIndex !== false) {
$k = substr($k, $lastIndex +1);
$k = substr($k, $lastIndex + 1);
}
$array[$k] = $v;
}

View File

@ -50,7 +50,8 @@ class PullPayment extends AbstractClient
?bool $autoApproveClaims = false,
?int $startsAt,
?int $expiresAt,
array $paymentMethods
?array $paymentMethods = null,
?string $description = null
): ResultPullPayment {
$url = $this->getApiUrl() . 'stores/' .
urlencode($storeId) . '/pull-payments';
@ -61,6 +62,7 @@ class PullPayment extends AbstractClient
$body = json_encode(
[
'name' => $name,
'description' => $description,
'amount' => $amount->__toString(),
'currency' => $currency,
'period' => $period,
@ -68,7 +70,8 @@ class PullPayment extends AbstractClient
'autoApproveClaims' => $autoApproveClaims,
'startsAt' => $startsAt,
'expiresAt' => $expiresAt,
'paymentMethods' => $paymentMethods
'paymentMethods' => $paymentMethods,
'payoutMethods' => $paymentMethods
],
JSON_THROW_ON_ERROR
);
@ -241,6 +244,7 @@ class PullPayment extends AbstractClient
'destination' => $destination,
'amount' => $amount->__toString(),
'paymentMethod' => $paymentMethod,
'payoutMethodId' => $paymentMethod, // BTCPay 2.0.0 compatibilty
],
JSON_THROW_ON_ERROR
);

View File

@ -20,7 +20,7 @@ class Store extends AbstractClient
int $paymentTolerance = 0,
bool $anyoneCanCreateInvoice = false,
bool $requiresRefundEmail = false,
?string $checkoutType = 'V1',
?string $checkoutType = 'V2',
?array $receipt = null,
bool $lightningAmountInSatoshi = false,
bool $lightningPrivateRouteHints = false,
@ -35,7 +35,15 @@ class Store extends AbstractClient
string $networkFeeMode = 'MultiplePaymentsOnly',
bool $payJoinEnabled = false,
bool $lazyPaymentMethods = false,
string $defaultPaymentMethod = 'BTC'
string $defaultPaymentMethod = 'BTC',
?string $supportUrl = null,
bool $archived = false,
bool $autodetectLanguage = false,
bool $showPayInWalletButton = true,
bool $showStoreHeader = true,
bool $celebratePayment = true,
bool $playSoundOnPayment = false,
?array $paymentMethodCriteria = null
): ResultStore {
$url = $this->getApiUrl() . 'stores';
$headers = $this->getRequestHeaders();
@ -45,6 +53,7 @@ class Store extends AbstractClient
[
"name" => $name,
"website" => $website,
"supportUrl" => $supportUrl,
"defaultCurrency" => $defaultCurrency,
"invoiceExpiration" => $invoiceExpiration,
"displayExpirationTimer" => $displayExpirationTimer,
@ -52,6 +61,7 @@ class Store extends AbstractClient
"speedPolicy" => $speedPolicy,
"lightningDescriptionTemplate" => $lightningDescriptionTemplate,
"paymentTolerance" => $paymentTolerance,
"archived" => $archived,
"anyoneCanCreateInvoice" => $anyoneCanCreateInvoice,
"requiresRefundEmail" => $requiresRefundEmail,
"checkoutType" => $checkoutType,
@ -68,8 +78,14 @@ class Store extends AbstractClient
"htmlTitle" => $htmlTitle,
"networkFeeMode" => $networkFeeMode,
"payJoinEnabled" => $payJoinEnabled,
"autodetectLanguage" => $autodetectLanguage,
"showPayInWalletButton" => $showPayInWalletButton,
"showStoreHeader" => $showStoreHeader,
"celebratePayment" => $celebratePayment,
"playSoundOnPayment" => $playSoundOnPayment,
"lazyPaymentMethods" => $lazyPaymentMethods,
"defaultPaymentMethod" => $defaultPaymentMethod
"defaultPaymentMethod" => $defaultPaymentMethod,
"paymentMethodCriteria" => $paymentMethodCriteria
],
JSON_THROW_ON_ERROR
);
@ -97,6 +113,115 @@ class Store extends AbstractClient
}
}
/**
* Update store settings. Make sure to pass all the settings, even if you don't want to change them.
*/
public function updateStore(
string $storeId,
string $name,
?string $website = null,
string $defaultCurrency = 'USD',
int $invoiceExpiration = 900,
int $displayExpirationTimer = 300,
int $monitoringExpiration = 3600,
string $speedPolicy = 'MediumSpeed',
?string $lightningDescriptionTemplate = null,
int $paymentTolerance = 0,
bool $anyoneCanCreateInvoice = false,
bool $requiresRefundEmail = false,
?string $checkoutType = 'V2',
?array $receipt = null,
bool $lightningAmountInSatoshi = false,
bool $lightningPrivateRouteHints = false,
bool $onChainWithLnInvoiceFallback = false,
bool $redirectAutomatically = false,
bool $showRecommendedFee = true,
int $recommendedFeeBlockTarget = 1,
string $defaultLang = 'en',
?string $customLogo = null,
?string $customCSS = null,
?string $htmlTitle = null,
string $networkFeeMode = 'MultiplePaymentsOnly',
bool $payJoinEnabled = false,
bool $lazyPaymentMethods = false,
string $defaultPaymentMethod = 'BTC',
?string $supportUrl = null,
bool $archived = false,
bool $autodetectLanguage = false,
bool $showPayInWalletButton = true,
bool $showStoreHeader = true,
bool $celebratePayment = true,
bool $playSoundOnPayment = false,
?array $paymentMethodCriteria = null
): ResultStore {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId);
$headers = $this->getRequestHeaders();
$method = 'PUT';
$body = json_encode(
[
"name" => $name,
"website" => $website,
"supportUrl" => $supportUrl,
"defaultCurrency" => $defaultCurrency,
"invoiceExpiration" => $invoiceExpiration,
"displayExpirationTimer" => $displayExpirationTimer,
"monitoringExpiration" => $monitoringExpiration,
"speedPolicy" => $speedPolicy,
"lightningDescriptionTemplate" => $lightningDescriptionTemplate,
"paymentTolerance" => $paymentTolerance,
"archived" => $archived,
"anyoneCanCreateInvoice" => $anyoneCanCreateInvoice,
"requiresRefundEmail" => $requiresRefundEmail,
"checkoutType" => $checkoutType,
"receipt" => $receipt,
"lightningAmountInSatoshi" => $lightningAmountInSatoshi,
"lightningPrivateRouteHints" => $lightningPrivateRouteHints,
"onChainWithLnInvoiceFallback" => $onChainWithLnInvoiceFallback,
"redirectAutomatically" => $redirectAutomatically,
"showRecommendedFee" => $showRecommendedFee,
"recommendedFeeBlockTarget" => $recommendedFeeBlockTarget,
"defaultLang" => $defaultLang,
"customLogo" => $customLogo,
"customCSS" => $customCSS,
"htmlTitle" => $htmlTitle,
"networkFeeMode" => $networkFeeMode,
"payJoinEnabled" => $payJoinEnabled,
"autodetectLanguage" => $autodetectLanguage,
"showPayInWalletButton" => $showPayInWalletButton,
"showStoreHeader" => $showStoreHeader,
"celebratePayment" => $celebratePayment,
"playSoundOnPayment" => $playSoundOnPayment,
"lazyPaymentMethods" => $lazyPaymentMethods,
"defaultPaymentMethod" => $defaultPaymentMethod,
"paymentMethodCriteria" => $paymentMethodCriteria
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new ResultStore(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function deleteStore(string $storeId): bool
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId);
$headers = $this->getRequestHeaders();
$method = 'DELETE';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return true;
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
/**
* @return \BTCPayServer\Result\Store[]
*/

View File

@ -35,6 +35,50 @@ class StoreOnChainWallet extends AbstractClient
}
}
public function createStoreOnChainWallet(
string $storeId,
string $cryptoCode,
?string $existingMnemonic = null,
?string $passphrase = null,
int $accountNumber = 0,
bool $savePrivateKeys = false,
bool $importKeysToRPC = false,
string $wordList = 'English',
int $wordCount = 12,
string $scriptPubKeyType = 'Segwit'
): ResultStoreOnChainWallet {
$url = $this->getApiUrl() . 'stores/' .
urlencode($storeId) . '/payment-methods/onchain/' .
urlencode($cryptoCode) . '/generate';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'existingMnemonic' => $existingMnemonic,
'passphrase' => $passphrase,
'accountNumber' => $accountNumber,
'savePrivateKeys' => $savePrivateKeys,
'importKeysToRPC' => $importKeysToRPC,
'wordList' => $wordList,
'wordCount' => $wordCount,
'scriptPubKeyType' => $scriptPubKeyType
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new ResultStoreOnChainWallet(
json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR)
);
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function getStoreOnChainWalletFeeRate(
string $storeId,
string $cryptoCode,

View File

@ -15,9 +15,12 @@ use BTCPayServer\Result\StorePaymentMethodCollection;
*/
class StorePaymentMethod extends AbstractClient
{
public function getPaymentMethods(string $storeId): array
public function getPaymentMethods(string $storeId, bool $includeConfig = false): array
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/payment-methods';
if ($includeConfig) {
$url .= '?includeConfig=true';
}
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);

View File

@ -10,6 +10,8 @@ use BTCPayServer\Result\StorePaymentMethodLightningNetwork as ResultStorePayment
* Handles a stores LightningNetwork payment methods.
*
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Store-Payment-Methods-(Lightning-Network)
*
* @deprecated with BTCPay 2.0. Use \BTCPayServer\Client\StorePaymentMethod->getPaymentMethods() instead.
*/
class StorePaymentMethodLightningNetwork extends AbstractStorePaymentMethodClient
{

View File

@ -10,6 +10,8 @@ use BTCPayServer\Result\StorePaymentMethodOnChain as ResultStorePaymentMethodOnC
* Handles stores on chain payment methods.
*
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Store-Payment-Methods-(On-Chain)
*
* @deprecated with BTCPay 2.0. Use \BTCPayServer\Client\StorePaymentMethod->getPaymentMethods() instead.
*/
class StorePaymentMethodOnChain extends AbstractStorePaymentMethodClient
{
@ -133,7 +135,7 @@ class StorePaymentMethodOnChain extends AbstractStorePaymentMethodClient
string $storeId,
string $cryptoCode,
string $derivationScheme,
string $accountKeyPath = null
?string $accountKeyPath = null
): array {
// todo: add offset + amount query parameters + check structure of derivationScheme etc.

View File

@ -97,7 +97,7 @@ class StoreRate extends AbstractClient
public function getRates(
string $storeId,
array $currencyPairs = null
?array $currencyPairs = null
): StoreRateList {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/rates?';
$headers = $this->getRequestHeaders();

View File

@ -0,0 +1,493 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Client;
use BTCPayServer\Result\Credit;
use BTCPayServer\Result\Offering;
use BTCPayServer\Result\OfferingList;
use BTCPayServer\Result\OfferingPlan;
use BTCPayServer\Result\PlanCheckout;
use BTCPayServer\Result\PortalSession;
use BTCPayServer\Result\Subscriber;
/**
* Handles subscriptions operations.
*
* @see https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Subscriptions
*/
class Subscriptions extends AbstractClient
{
// Offering endpoints
public function getOffering(string $storeId, string $offeringId): Offering
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new Offering(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function getOfferings(string $storeId): OfferingList
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings';
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new OfferingList(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function createOffering(
string $storeId,
?string $appName = null,
?string $successRedirectUrl = null,
?array $metadata = null,
?array $features = null
): Offering {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'appName' => $appName,
'successRedirectUrl' => $successRedirectUrl,
'metadata' => $metadata,
'features' => $features
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200 || $response->getStatus() === 201) {
return new Offering(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function updateOffering(
string $storeId,
string $offeringId,
?string $appName = null,
?string $successRedirectUrl = null,
?array $metadata = null,
?array $features = null
): Offering {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId);
$headers = $this->getRequestHeaders();
$method = 'PUT';
$body = json_encode(
[
'appName' => $appName,
'successRedirectUrl' => $successRedirectUrl,
'metadata' => $metadata,
'features' => $features
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new Offering(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
// Plan endpoints
public function createOfferingPlan(
string $storeId,
string $offeringId,
?string $description = null,
?string $currency = null,
?int $gracePeriodDays = null,
?string $name = null,
?bool $optimisticActivation = null,
?string $price = null,
?bool $renewable = null,
?int $trialDays = null,
?array $metadata = null,
?string $recurringType = null,
?array $features = null
): OfferingPlan {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/plans';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'description' => $description,
'currency' => $currency,
'gracePeriodDays' => $gracePeriodDays,
'name' => $name,
'optimisticActivation' => $optimisticActivation,
'price' => $price,
'renewable' => $renewable,
'trialDays' => $trialDays,
'metadata' => $metadata,
'recurringType' => $recurringType,
'features' => $features
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200 || $response->getStatus() === 201) {
return new OfferingPlan(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function getOfferingPlan(string $storeId, string $offeringId, string $planId): OfferingPlan
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/plans/' . urlencode($planId);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new OfferingPlan(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function updateOfferingPlan(
string $storeId,
string $offeringId,
string $planId,
?string $description = null,
?string $currency = null,
?int $gracePeriodDays = null,
?string $name = null,
?bool $optimisticActivation = null,
?string $price = null,
?bool $renewable = null,
?int $trialDays = null,
?array $metadata = null,
?string $recurringType = null,
?array $features = null
): OfferingPlan {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/plans/' . urlencode($planId);
$headers = $this->getRequestHeaders();
$method = 'PUT';
$body = json_encode(
[
'description' => $description,
'currency' => $currency,
'gracePeriodDays' => $gracePeriodDays,
'name' => $name,
'optimisticActivation' => $optimisticActivation,
'price' => $price,
'renewable' => $renewable,
'trialDays' => $trialDays,
'metadata' => $metadata,
'recurringType' => $recurringType,
'features' => $features
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new OfferingPlan(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
// Subscriber endpoints
public function getSubscriber(string $storeId, string $offeringId, string $customerSelector): Subscriber
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new Subscriber(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function deleteSubscriber(string $storeId, string $offeringId, string $customerSelector): void
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector);
$headers = $this->getRequestHeaders();
$method = 'DELETE';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() !== 204) {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function updateSubscriberDates(
string $storeId,
string $offeringId,
string $customerSelector,
?int $startDate = null,
?int $expirationDate = null
): Subscriber {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector) . '/dates';
$headers = $this->getRequestHeaders();
$method = 'PUT';
$body = json_encode(
[
'startDate' => $startDate,
'expirationDate' => $expirationDate
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new Subscriber(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function suspendSubscriber(string $storeId, string $offeringId, string $customerSelector, string $reason): Subscriber
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector) . '/suspend';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'reason' => $reason
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new Subscriber(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function unsuspendSubscriber(string $storeId, string $offeringId, string $customerSelector): Subscriber
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector) . '/unsuspend';
$headers = $this->getRequestHeaders();
$method = 'POST';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new Subscriber(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
// Credit endpoints
public function getCredit(string $storeId, string $offeringId, string $customerSelector, string $currency): Credit
{
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector) . '/credits/' . urlencode($currency);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new Credit(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function updateCredit(
string $storeId,
string $offeringId,
string $customerSelector,
string $currency,
?string $credit = null,
?string $charge = null,
?string $description = null,
?bool $allowOverdraft = null
): Credit {
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/offerings/' . urlencode($offeringId) . '/subscribers/' . urlencode($customerSelector) . '/credits/' . urlencode($currency);
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'credit' => $credit,
'charge' => $charge,
'description' => $description,
'allowOverdraft' => $allowOverdraft
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new Credit(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
// Plan checkout endpoints
public function getPlanCheckout(string $checkoutId): PlanCheckout
{
$url = $this->getApiUrl() . 'plan-checkout/' . urlencode($checkoutId);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new PlanCheckout(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function proceedPlanCheckout(string $checkoutId, ?string $email = null): PlanCheckout
{
$url = $this->getApiUrl() . 'plan-checkout/' . urlencode($checkoutId);
$headers = $this->getRequestHeaders();
$method = 'POST';
$params = [];
if ($email !== null) {
$params['email'] = $email;
}
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new PlanCheckout(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function createPlanCheckout(
string $storeId,
string $offeringId,
string $planId,
?string $customerSelector = null,
?int $durationMinutes = null,
?string $onPayBehavior = null,
?array $newSubscriberMetadata = null,
?array $invoiceMetadata = null,
?array $metadata = null,
?bool $isTrial = null,
?string $creditPurchase = null,
?string $successRedirectLink = null,
?string $newSubscriberEmail = null
): PlanCheckout {
$url = $this->getApiUrl() . 'plan-checkout';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'storeId' => $storeId,
'offeringId' => $offeringId,
'planId' => $planId,
'customerSelector' => $customerSelector,
'durationMinutes' => $durationMinutes,
'onPayBehavior' => $onPayBehavior,
'newSubscriberMetadata' => $newSubscriberMetadata,
'invoiceMetadata' => $invoiceMetadata,
'metadata' => $metadata,
'isTrial' => $isTrial,
'creditPurchase' => $creditPurchase,
'successRedirectLink' => $successRedirectLink,
'newSubscriberEmail' => $newSubscriberEmail
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new PlanCheckout(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
// Portal session endpoints
public function createPortalSession(
string $storeId,
string $offeringId,
string $customerSelector,
?int $durationMinutes = null
): PortalSession {
$url = $this->getApiUrl() . 'subscriber-portal';
$headers = $this->getRequestHeaders();
$method = 'POST';
$body = json_encode(
[
'storeId' => $storeId,
'offeringId' => $offeringId,
'customerSelector' => $customerSelector,
'durationMinutes' => $durationMinutes
],
JSON_THROW_ON_ERROR
);
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
return new PortalSession(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
public function getPortalSession(string $portalSessionId): PortalSession
{
$url = $this->getApiUrl() . 'subscriber-portal/' . urlencode($portalSessionId);
$headers = $this->getRequestHeaders();
$method = 'GET';
$response = $this->getHttpClient()->request($method, $url, $headers);
if ($response->getStatus() === 200) {
return new PortalSession(json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR));
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
}

View File

@ -59,7 +59,7 @@ class User extends AbstractClient
$response = $this->getHttpClient()->request($method, $url, $headers, $body);
if ($response->getStatus() === 200) {
if ($response->getStatus() === 201) {
return new ResultUser(
json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR)
);

View File

@ -238,7 +238,7 @@ class Webhook extends AbstractClient
if ($requestBody && $btcpaySigHeader) {
$expectedHeader = 'sha256=' . hash_hmac('sha256', $requestBody, $secret);
if ($expectedHeader === $btcpaySigHeader) {
if (hash_equals($expectedHeader, $btcpaySigHeader)) {
return true;
}
}

View File

@ -6,7 +6,7 @@ namespace BTCPayServer\Exception;
class BTCPayException extends \RuntimeException
{
public function __construct(string $message, int $code, \Throwable $previous = null)
public function __construct(string $message, int $code, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}

View File

@ -6,7 +6,7 @@ namespace BTCPayServer\Result;
abstract class AbstractStorePaymentMethodResult extends AbstractResult
{
public function __construct(array $data, string $paymentMethod = null)
public function __construct(array $data, ?string $paymentMethod = null)
{
// Temporary workaround until the api provides paymentMethod.
if (!isset($data['paymentMethod'])) {

View File

@ -6,4 +6,18 @@ namespace BTCPayServer\Result;
class ApiKey extends AbstractResult
{
public function getApiKey(): string
{
return $this->getData()['apiKey'];
}
public function getLabel(): string
{
return $this->getData()['label'];
}
public function getPermissions(): array
{
return $this->getData()['permissions'];
}
}

18
src/Result/Credit.php Normal file
View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class Credit extends AbstractResult
{
public function getCurrency(): string
{
return $this->getData()['currency'];
}
public function getValue(): string
{
return $this->getData()['value'];
}
}

33
src/Result/Customer.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class Customer extends AbstractResult
{
public function getStoreId(): string
{
return $this->getData()['storeId'];
}
public function getId(): string
{
return $this->getData()['id'];
}
public function getExternalId(): ?string
{
return $this->getData()['externalId'] ?? null;
}
public function getIdentities(): ?array
{
return $this->getData()['identities'] ?? null;
}
public function getMetadata(): ?array
{
return $this->getData()['metadata'] ?? null;
}
}

18
src/Result/Feature.php Normal file
View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class Feature extends AbstractResult
{
public function getId(): string
{
return $this->getData()['id'];
}
public function getDescription(): string
{
return $this->getData()['description'];
}
}

View File

@ -59,25 +59,37 @@ class InvoicePaymentMethod extends AbstractResult
public function getNetworkFee(): string
{
$data = $this->getData();
return $data['networkFee'];
// BTCPay 2.0.0 compatibility: networkFee was renamed to paymentMethodFee.
return $data['networkFee'] ?? $data['paymentMethodFee'];
}
public function getPaymentMethod(): string
{
$data = $this->getData();
return $data['paymentMethod'];
// BTCPay 2.0.0 compatibility: paymentMethod was renamed to paymentMethodId.
return $data['paymentMethod'] ?? $data['paymentMethodId'];
}
public function getCryptoCode(): string
{
$data = $this->getData();
// For future compatibility check if cryptoCode exists.
if (isset($data['cryptoCode'])) {
return $data['cryptoCode'];
} else {
// Extract cryptoCode from paymentMethod string.
$parts = explode('-', $data['paymentMethod']);
$parts = explode('-', $this->getPaymentMethod());
return $parts[0];
}
}
/**
* New field as of BTCPay 2.0.0.
*/
public function getCurrency(): ?string
{
$data = $this->getData();
return $data['currency'] ?? null;
}
}

66
src/Result/Offering.php Normal file
View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class Offering extends AbstractResult
{
public function getId(): string
{
return $this->getData()['id'];
}
public function getStoreId(): string
{
return $this->getData()['storeId'];
}
public function getAppId(): ?string
{
return $this->getData()['appId'] ?? null;
}
public function getAppName(): ?string
{
return $this->getData()['appName'] ?? null;
}
public function getSuccessRedirectUrl(): ?string
{
return $this->getData()['successRedirectUrl'] ?? null;
}
public function getMetadata(): ?array
{
return $this->getData()['metadata'] ?? null;
}
/**
* @return OfferingPlan[]
*/
public function getPlans(): array
{
$plans = [];
if (isset($this->getData()['plans']) && is_array($this->getData()['plans'])) {
foreach ($this->getData()['plans'] as $plan) {
$plans[] = new OfferingPlan($plan);
}
}
return $plans;
}
/**
* @return Feature[]
*/
public function getFeatures(): array
{
$features = [];
if (isset($this->getData()['features']) && is_array($this->getData()['features'])) {
foreach ($this->getData()['features'] as $feature) {
$features[] = new Feature($feature);
}
}
return $features;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class OfferingList extends AbstractListResult
{
/**
* @return Offering[]
*/
public function all(): array
{
$result = [];
foreach ($this->getData() as $item) {
$result[] = new Offering($item);
}
return $result;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class OfferingPlan extends AbstractResult
{
public function getId(): string
{
return $this->getData()['id'];
}
public function getName(): string
{
return $this->getData()['name'];
}
public function getStatus(): string
{
return $this->getData()['status'];
}
public function getPrice(): string
{
return $this->getData()['price'];
}
public function getCurrency(): string
{
return $this->getData()['currency'];
}
public function getRecurringType(): string
{
return $this->getData()['recurringType'];
}
public function getGracePeriodDays(): int
{
return $this->getData()['gracePeriodDays'];
}
public function getTrialDays(): int
{
return $this->getData()['trialDays'];
}
public function getDescription(): string
{
return $this->getData()['description'];
}
public function getMemberCount(): int
{
return $this->getData()['memberCount'];
}
public function isOptimisticActivation(): bool
{
return $this->getData()['optimisticActivation'];
}
public function isRenewable(): bool
{
return $this->getData()['renewable'];
}
/**
* @return string[]
*/
public function getFeatures(): array
{
return $this->getData()['features'] ?? [];
}
public function getMetadata(): ?array
{
return $this->getData()['metadata'] ?? null;
}
}

113
src/Result/PlanCheckout.php Normal file
View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class PlanCheckout extends AbstractResult
{
public function getId(): string
{
return $this->getData()['id'];
}
public function getSubscriber(): ?Subscriber
{
return isset($this->getData()['subscriber']) ? new Subscriber($this->getData()['subscriber']) : null;
}
public function getPlan(): OfferingPlan
{
return new OfferingPlan($this->getData()['plan']);
}
public function getBaseUrl(): string
{
return $this->getData()['baseUrl'];
}
public function getInvoiceId(): ?string
{
return $this->getData()['invoiceId'] ?? null;
}
public function getSuccessRedirectUrl(): ?string
{
return $this->getData()['successRedirectUrl'] ?? null;
}
public function getExpiration(): int
{
return $this->getData()['expiration'];
}
public function getRedirectUrl(): string
{
return $this->getData()['redirectUrl'];
}
public function getInvoiceMetadata(): ?array
{
return $this->getData()['invoiceMetadata'] ?? null;
}
public function getMetadata(): ?array
{
return $this->getData()['metadata'] ?? null;
}
public function isNewSubscriber(): bool
{
return $this->getData()['newSubscriber'];
}
public function isTrial(): bool
{
return $this->getData()['isTrial'];
}
public function getCreated(): int
{
return $this->getData()['created'];
}
public function isPlanStarted(): bool
{
return $this->getData()['planStarted'];
}
public function getNewSubscriberMetadata(): ?array
{
return $this->getData()['newSubscriberMetadata'] ?? null;
}
public function getRefundAmount(): ?string
{
return $this->getData()['refundAmount'] ?? null;
}
public function getCreditedByInvoice(): ?string
{
return $this->getData()['creditedByInvoice'] ?? null;
}
public function getOnPayBehavior(): ?string
{
return $this->getData()['onPayBehavior'] ?? null;
}
public function isExpired(): bool
{
return $this->getData()['isExpired'];
}
public function getUrl(): string
{
return $this->getData()['url'];
}
public function getCreditPurchase(): ?string
{
return $this->getData()['creditPurchase'] ?? null;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class PortalSession extends AbstractResult
{
public function getId(): string
{
return $this->getData()['id'];
}
public function getBaseUrl(): string
{
return $this->getData()['baseUrl'];
}
public function getSubscriber(): Subscriber
{
return new Subscriber($this->getData()['subscriber']);
}
public function getExpiration(): ?int
{
return $this->getData()['expiration'] ?? null;
}
public function isExpired(): bool
{
return $this->getData()['isExpired'];
}
public function getUrl(): string
{
return $this->getData()['url'];
}
}

View File

@ -20,6 +20,12 @@ class PullPayment extends AbstractResult
return $data['name'];
}
public function getDescription(): string
{
$data = $this->getData();
return $data['description'];
}
public function getCurrency(): string
{
$data = $this->getData();

View File

@ -13,17 +13,40 @@ class StorePaymentMethodCollection extends AbstractListResult
{
$r = [];
foreach ($this->getData() as $paymentMethod => $paymentMethodData) {
// BTCPay 2.0 compatibility: List is not a keyed array anymore so fix it here.
if (is_numeric($paymentMethod)) {
$paymentMethod = $paymentMethodData['paymentMethodId'];
// Extract the cryptoCode from the paymentMethodId. e.g. "BTC-CHAIN" -> "BTC"
$parts = explode('-', $paymentMethod);
$extractedCryptoCode = $parts[0];
}
// Consistency: Flatten the array to be consistent with the specific
// payment method endpoints.
$paymentMethodData += $paymentMethodData['data'];
unset($paymentMethodData['data']);
if (isset($paymentMethodData['data'])) {
$paymentMethodData += $paymentMethodData['data'];
unset($paymentMethodData['data']);
}
if (strpos($paymentMethod, 'LightningNetwork') !== false) {
// BTCPay 2.0 compatibility: Handle config data if exists.
if (isset($paymentMethodData['config'])) {
$paymentMethodData += $paymentMethodData['config'];
unset($paymentMethodData['config']);
}
// BTCPay 2.0 compatibility: Check for renamed LN payment method id.
if (preg_match('/(LightningNetwork|-LN$)/', $paymentMethod)) {
// Consistency: Add back the cryptoCode missing on this endpoint
// results until it is there.
if (!isset($paymentMethodData['cryptoCode'])) {
$paymentMethodData['cryptoCode'] = str_replace('-LightningNetwork', '', $paymentMethod);
}
// BTCPay 2.0 compatibility: put the extracted cryptoCode in the cryptoCode field.
if (isset($extractedCryptoCode)) {
$paymentMethodData['cryptoCode'] = $extractedCryptoCode;
}
$r[] = new StorePaymentMethodLightningNetwork($paymentMethodData, $paymentMethod);
} else {
// Consistency: Add back the cryptoCode missing on this endpoint
@ -31,6 +54,12 @@ class StorePaymentMethodCollection extends AbstractListResult
if (!isset($paymentMethodData['cryptoCode'])) {
$paymentMethodData['cryptoCode'] = $paymentMethod;
}
// BTCPay 2.0 compatibility: put the currency code in the cryptoCode field.
if (isset($extractedCryptoCode)) {
$paymentMethodData['cryptoCode'] = $extractedCryptoCode;
}
$r[] = new StorePaymentMethodOnChain($paymentMethodData, $paymentMethod);
}
}

93
src/Result/Subscriber.php Normal file
View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace BTCPayServer\Result;
class Subscriber extends AbstractResult
{
public function getCreated(): int
{
return $this->getData()['created'];
}
public function getCustomer(): Customer
{
return new Customer($this->getData()['customer']);
}
public function getOffering(): Offering
{
return new Offering($this->getData()['offering']);
}
public function getPlan(): OfferingPlan
{
return new OfferingPlan($this->getData()['plan']);
}
public function getPeriodEnd(): ?int
{
return $this->getData()['periodEnd'] ?? null;
}
public function getTrialEnd(): ?int
{
return $this->getData()['trialEnd'] ?? null;
}
public function getGracePeriodEnd(): ?int
{
return $this->getData()['gracePeriodEnd'] ?? null;
}
public function isActive(): bool
{
return $this->getData()['isActive'];
}
public function isSuspended(): bool
{
return $this->getData()['isSuspended'];
}
public function getSuspensionReason(): ?string
{
return $this->getData()['suspensionReason'] ?? null;
}
public function isAutoRenew(): bool
{
return $this->getData()['autoRenew'];
}
public function getMetadata(): ?array
{
return $this->getData()['metadata'] ?? null;
}
public function getProcessingInvoiceId(): ?string
{
return $this->getData()['processingInvoiceId'] ?? null;
}
public function getNextPlan(): ?OfferingPlan
{
return isset($this->getData()['nextPlan']) ? new OfferingPlan($this->getData()['nextPlan']) : null;
}
public function getScheduledPlan(): ?OfferingPlan
{
return isset($this->getData()['scheduledPlan']) ? new OfferingPlan($this->getData()['scheduledPlan']) : null;
}
public function getScheduledPlanActivatesAt(): ?int
{
return $this->getData()['scheduledPlanActivatesAt'] ?? null;
}
public function getPhase(): string
{
return $this->getData()['phase'];
}
}