mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
improve the code a bit
This commit is contained in:
@@ -41,7 +41,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'actorFeatures',
|
||||
name: 'get',
|
||||
description: 'Get features available to an actor (user/team) based on their subscription plan',
|
||||
description: <<<EOT
|
||||
Get the features available to a specific actor (user or team) based on their active subscription.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -41,7 +41,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'features',
|
||||
name: 'create',
|
||||
description: 'Create a feature definition',
|
||||
description: <<<EOT
|
||||
Create a new feature that can be assigned to plans. Features can be boolean (on/off) or metered (usage-based).
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -39,7 +39,9 @@ class Delete extends Base
|
||||
namespace: 'payments',
|
||||
group: 'features',
|
||||
name: 'delete',
|
||||
description: 'Delete a feature definition',
|
||||
description: <<<EOT
|
||||
Delete a feature by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -35,7 +35,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'features',
|
||||
name: 'get',
|
||||
description: 'Get a payment feature',
|
||||
description: <<<EOT
|
||||
Get a feature by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -39,7 +39,9 @@ class Update extends Base
|
||||
namespace: 'payments',
|
||||
group: 'features',
|
||||
name: 'update',
|
||||
description: 'Update a feature definition',
|
||||
description: <<<EOT
|
||||
Update a feature by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -2,16 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Features;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\PaymentFeatures;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Base
|
||||
@@ -36,7 +42,9 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'features',
|
||||
name: 'list',
|
||||
description: 'List feature definitions',
|
||||
description: <<<EOT
|
||||
Get a list of all features configured for the project.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -45,25 +53,68 @@ class XList extends Base
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('search', '', new Text(256), 'Search term.', true)
|
||||
->param('queries', [], new PaymentFeatures(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', PaymentFeatures::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$filters = [];
|
||||
if ($search !== '') {
|
||||
$filters[] = Query::search('name', $search);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
$list = $dbForProject->find('payments_features', $filters);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$featureId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('payments_features', $featureId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Feature '{$featureId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$features = $dbForProject->find('payments_features', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('payments_features', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => count($list),
|
||||
'features' => $list
|
||||
'features' => $features,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PAYMENT_FEATURE_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Invoices;
|
||||
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Payments\Provider\StripeAdapter;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Payments\Provider\ProviderState;
|
||||
use Appwrite\Payments\Provider\ProviderSubscriptionRef;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -40,18 +41,20 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'listInvoices',
|
||||
description: 'List invoices for a subscription',
|
||||
description: <<<EOT
|
||||
Get a list of invoices for a subscription from the payment provider.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_ANY,
|
||||
model: Response::MODEL_PAYMENT_INVOICE_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('subscriptionId', '', new Text(128), 'Subscription ID')
|
||||
->param('limit', 25, new Integer(true), 'Maximum number of invoices to return (max 100)', true)
|
||||
->param('offset', 0, new Integer(true), 'Offset for pagination', true)
|
||||
->param('subscriptionId', '', new Text(128), 'Subscription ID.')
|
||||
->param('limit', 25, new Integer(true), 'Maximum number of invoices to return. Max 100.', true)
|
||||
->param('offset', 0, new Integer(true), 'Offset for pagination.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -76,23 +79,17 @@ class XList extends Base
|
||||
$projDoc = $dbForPlatform->getDocument('projects', $project->getId());
|
||||
$paymentsCfg = (array) $projDoc->getAttribute('payments', []);
|
||||
if (isset($paymentsCfg['enabled']) && $paymentsCfg['enabled'] === false) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_FORBIDDEN);
|
||||
$response->json(['message' => 'Payments feature is disabled for this project']);
|
||||
return;
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Payments feature is disabled for this project');
|
||||
}
|
||||
|
||||
// Validate limit
|
||||
if ($limit < 1 || $limit > 100) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
|
||||
$response->json(['message' => 'Limit must be between 1 and 100']);
|
||||
return;
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Limit must be between 1 and 100');
|
||||
}
|
||||
|
||||
// Validate offset
|
||||
if ($offset < 0) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
|
||||
$response->json(['message' => 'Offset must be non-negative']);
|
||||
return;
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Offset must be non-negative');
|
||||
}
|
||||
|
||||
// Get subscription from database
|
||||
@@ -101,9 +98,7 @@ class XList extends Base
|
||||
]);
|
||||
|
||||
if ($subscription === null || $subscription->isEmpty()) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_NOT_FOUND);
|
||||
$response->json(['message' => 'Subscription not found']);
|
||||
return;
|
||||
throw new Exception(Exception::PAYMENT_SUBSCRIPTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Authorization: only enforce for JWT users, API keys have admin access
|
||||
@@ -113,7 +108,7 @@ class XList extends Base
|
||||
|
||||
if ($actorType === 'user') {
|
||||
if ($user->getId() !== $actorId) {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::USER_UNAUTHORIZED, 'Not authorized to access this subscription');
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'Not authorized to access this subscription');
|
||||
}
|
||||
} elseif ($actorType === 'team') {
|
||||
$membership = $dbForProject->findOne('memberships', [
|
||||
@@ -122,49 +117,50 @@ class XList extends Base
|
||||
]);
|
||||
|
||||
if ($membership === null || $membership->isEmpty()) {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::USER_UNAUTHORIZED, 'User is not a member of this team');
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not a member of this team');
|
||||
}
|
||||
|
||||
$roles = (array) $membership->getAttribute('roles', []);
|
||||
if (!in_array('owner', $roles, true) && !in_array('billing', $roles, true)) {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::USER_UNAUTHORIZED, 'User must have owner or billing role');
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'User must have owner or billing role');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get provider subscription ID
|
||||
$providerSubscriptionId = (string) $subscription->getAttribute('providerSubscriptionId', '');
|
||||
// Get provider from subscription
|
||||
$subscriptionProviders = (array) $subscription->getAttribute('providers', []);
|
||||
$providerId = array_key_first($subscriptionProviders);
|
||||
|
||||
if (!$providerId) {
|
||||
// No provider on subscription, return empty list
|
||||
$response->dynamic(new Document([
|
||||
'invoices' => [],
|
||||
'total' => 0,
|
||||
]), Response::MODEL_PAYMENT_INVOICE_LIST);
|
||||
return;
|
||||
}
|
||||
|
||||
$providerData = (array) ($subscriptionProviders[$providerId] ?? []);
|
||||
$providerSubscriptionId = (string) ($providerData['providerSubscriptionId'] ?? '');
|
||||
|
||||
if ($providerSubscriptionId === '') {
|
||||
// No provider subscription yet, return empty list
|
||||
$response->setStatusCode(Response::STATUS_CODE_OK);
|
||||
$response->json([
|
||||
$response->dynamic(new Document([
|
||||
'invoices' => [],
|
||||
'total' => 0,
|
||||
'invoices' => []
|
||||
]);
|
||||
]), Response::MODEL_PAYMENT_INVOICE_LIST);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get payment provider
|
||||
$payments = (array) $project->getAttribute('payments', []);
|
||||
$providers = (array) ($payments['providers'] ?? []);
|
||||
$primary = array_key_first($providers);
|
||||
|
||||
if (!$primary) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
|
||||
$response->json(['message' => 'No payment provider configured for this project']);
|
||||
return;
|
||||
// Get payment provider configuration
|
||||
$providers = (array) ($paymentsCfg['providers'] ?? []);
|
||||
if (!isset($providers[$providerId])) {
|
||||
throw new Exception(Exception::PAYMENT_PROVIDER_NOT_CONFIGURED, 'Payment provider not configured for this project');
|
||||
}
|
||||
|
||||
$config = (array) ($providers[$primary] ?? []);
|
||||
$adapter = $registryPayments->get((string) $primary, $config, $project, $dbForPlatform, $dbForProject);
|
||||
$state = new \Appwrite\Payments\Provider\ProviderState((string) $primary, $config, (array) ($config['state'] ?? []));
|
||||
|
||||
if (!$adapter instanceof StripeAdapter) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
|
||||
$response->json(['message' => 'Unsupported payment provider: ' . $primary]);
|
||||
return;
|
||||
}
|
||||
$config = (array) ($providers[$providerId] ?? []);
|
||||
$adapter = $registryPayments->get((string) $providerId, $config, $projDoc, $dbForPlatform, $dbForProject);
|
||||
$state = new ProviderState((string) $providerId, $config, (array) ($config['state'] ?? []));
|
||||
|
||||
// Create provider subscription reference
|
||||
$providerSubRef = new ProviderSubscriptionRef(
|
||||
@@ -174,15 +170,13 @@ class XList extends Base
|
||||
try {
|
||||
$invoices = $adapter->listInvoices($providerSubRef, $state, $limit, $offset);
|
||||
} catch (\Throwable $e) {
|
||||
$response->setStatusCode(Response::STATUS_CODE_INTERNAL_SERVER_ERROR);
|
||||
$response->json(['message' => 'Failed to fetch invoices: ' . $e->getMessage()]);
|
||||
return;
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to fetch invoices: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Format invoices for response
|
||||
$formattedInvoices = [];
|
||||
foreach ($invoices as $invoice) {
|
||||
$formattedInvoices[] = [
|
||||
$formattedInvoices[] = new Document([
|
||||
'invoiceId' => $invoice->invoiceId,
|
||||
'subscriptionId' => $invoice->subscriptionId,
|
||||
'amount' => $invoice->amount,
|
||||
@@ -192,13 +186,12 @@ class XList extends Base
|
||||
'paidAt' => $invoice->paidAt,
|
||||
'invoiceUrl' => $invoice->invoiceUrl,
|
||||
'metadata' => $invoice->metadata,
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_OK);
|
||||
$response->json([
|
||||
$response->dynamic(new Document([
|
||||
'invoices' => $formattedInvoices,
|
||||
'total' => count($formattedInvoices),
|
||||
'invoices' => $formattedInvoices
|
||||
]);
|
||||
]), Response::MODEL_PAYMENT_INVOICE_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Payments\Provider\ProviderState;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Platform\Modules\Payments\Validator\FeatureTier;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -17,7 +18,6 @@ use Utopia\Database\Query;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
@@ -46,7 +46,9 @@ class Assign extends Base
|
||||
namespace: 'payments',
|
||||
group: 'planFeatures',
|
||||
name: 'assign',
|
||||
description: 'Assign feature to plan',
|
||||
description: <<<EOT
|
||||
Assign a feature to a plan with optional usage limits and pricing tiers for metered features.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -61,7 +63,7 @@ class Assign extends Base
|
||||
->param('interval', '', new Text(16, 0), 'Billing interval', true)
|
||||
->param('includedUnits', 0, new Integer(), 'Included units', true)
|
||||
->param('tiersMode', null, new Text(32, 0), 'Tiers mode (graduated or volume)', true)
|
||||
->param('tiers', [], new ArrayList(new Assoc(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Pricing tiers', true)
|
||||
->param('tiers', [], new ArrayList(new FeatureTier(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Pricing tiers', true)
|
||||
->param('usageCap', null, new Integer(), 'Usage cap', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
|
||||
@@ -42,7 +42,9 @@ class Remove extends Base
|
||||
namespace: 'payments',
|
||||
group: 'planFeatures',
|
||||
name: 'remove',
|
||||
description: 'Remove feature from plan',
|
||||
description: <<<EOT
|
||||
Remove a feature from a plan.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\PlanFeatures;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\PaymentPlanFeatures;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Base
|
||||
@@ -35,16 +42,21 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'planFeatures',
|
||||
name: 'list',
|
||||
description: 'List features assigned to a plan',
|
||||
description: <<<EOT
|
||||
Get a list of all features assigned to a specific plan.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_ANY,
|
||||
model: Response::MODEL_PAYMENT_PLAN_FEATURE_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('planId', '', new Text(128), 'Plan ID')
|
||||
->param('queries', [], new PaymentPlanFeatures(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', PaymentPlanFeatures::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
@@ -52,15 +64,62 @@ class XList extends Base
|
||||
|
||||
public function action(
|
||||
string $planId,
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$items = $dbForProject->find('payments_plan_features', [
|
||||
Query::equal('planId', [$planId])
|
||||
]);
|
||||
$response->json([
|
||||
'total' => count($items),
|
||||
'features' => array_map(fn ($d) => $d->getArrayCopy(), $items)
|
||||
]);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Always filter by planId
|
||||
$queries[] = Query::equal('planId', [$planId]);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$planFeatureId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('payments_plan_features', $planFeatureId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Plan feature '{$planFeatureId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$planFeatures = $dbForProject->find('payments_plan_features', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('payments_plan_features', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'features' => $planFeatures,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PAYMENT_PLAN_FEATURE_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@ use Appwrite\Extend\Exception as ExtendException;
|
||||
use Appwrite\Payments\Provider\ProviderState;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Platform\Modules\Payments\Validator\PricingEntry;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Exception;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\JSON as JSONValidator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Base
|
||||
@@ -48,7 +48,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'plans',
|
||||
name: 'create',
|
||||
description: 'Create a payment plan',
|
||||
description: <<<EOT
|
||||
Create a new payment plan for your project. Plans define pricing tiers and can be associated with features for subscription management.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -62,7 +64,7 @@ class Create extends Base
|
||||
->param('description', '', new Text(8192, 0), 'Plan description.', true)
|
||||
->param('isDefault', false, new Boolean(), 'Set as default plan for new users.', true)
|
||||
->param('isFree', false, new Boolean(), 'Is the plan free.', true)
|
||||
->param('pricing', [], new JSONValidator(), 'Pricing configuration array [{priceId,amount,currency,interval}]', true)
|
||||
->param('pricing', [], new ArrayList(new PricingEntry(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Pricing configuration array [{priceId,amount,currency,interval}]', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -86,31 +88,21 @@ class Create extends Base
|
||||
Event $queueForEvents,
|
||||
Document $project
|
||||
) {
|
||||
// Normalize and validate pricing definitions (ensure user-provided priceIds)
|
||||
$normalizedPricing = [];
|
||||
// Check for duplicate priceIds
|
||||
$seenPriceIds = [];
|
||||
foreach ($pricing as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
throw new AppwriteException(ExtendException::GENERAL_BAD_REQUEST, 'Invalid pricing entry format');
|
||||
}
|
||||
$priceId = (string) ($entry['priceId'] ?? '');
|
||||
if ($priceId === '') {
|
||||
throw new AppwriteException(ExtendException::GENERAL_BAD_REQUEST, 'Each pricing entry must include a priceId');
|
||||
}
|
||||
if (isset($seenPriceIds[$priceId])) {
|
||||
throw new AppwriteException(ExtendException::GENERAL_BAD_REQUEST, 'Duplicate priceId detected: ' . $priceId);
|
||||
}
|
||||
$seenPriceIds[$priceId] = true;
|
||||
$normalizedPricing[] = $entry;
|
||||
}
|
||||
|
||||
$pricing = $normalizedPricing;
|
||||
|
||||
$document = new Document([
|
||||
'planId' => $planId,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'pricing' => $normalizedPricing,
|
||||
'pricing' => $pricing,
|
||||
'isDefault' => $isDefault,
|
||||
'isFree' => $isFree,
|
||||
'status' => 'active',
|
||||
|
||||
@@ -39,7 +39,9 @@ class Delete extends Base
|
||||
namespace: 'payments',
|
||||
group: 'plans',
|
||||
name: 'delete',
|
||||
description: 'Delete a payment plan',
|
||||
description: <<<EOT
|
||||
Delete a payment plan by its unique ID. This action cannot be undone.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -35,7 +35,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'plans',
|
||||
name: 'get',
|
||||
description: 'Get a payment plan',
|
||||
description: <<<EOT
|
||||
Get a payment plan by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Payments\Http\Plans;
|
||||
use Appwrite\Payments\Provider\ProviderState;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Platform\Modules\Payments\Validator\PricingEntry;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -14,8 +15,8 @@ use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\JSON as JSONValidator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Base
|
||||
@@ -43,7 +44,9 @@ class Update extends Base
|
||||
namespace: 'payments',
|
||||
group: 'plans',
|
||||
name: 'update',
|
||||
description: 'Update a payment plan',
|
||||
description: <<<EOT
|
||||
Update a payment plan by its unique ID. You can update the plan name, description, and pricing configuration.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -57,7 +60,7 @@ class Update extends Base
|
||||
->param('description', '', new Text(8192, 0), 'Plan description.', true)
|
||||
->param('isDefault', false, new Boolean(), 'Set as default plan for new users.', true)
|
||||
->param('isFree', false, new Boolean(), 'Is the plan free.', true)
|
||||
->param('pricing', [], new JSONValidator(), 'Pricing configuration array [{priceId,amount,currency,interval}]', true)
|
||||
->param('pricing', [], new ArrayList(new PricingEntry(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Pricing configuration array [{priceId,amount,currency,interval}]', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -101,23 +104,15 @@ class Update extends Base
|
||||
$plan->setAttribute('isDefault', $isDefault);
|
||||
$plan->setAttribute('isFree', $isFree);
|
||||
if (!empty($pricing)) {
|
||||
$normalizedPricing = [];
|
||||
// Check for duplicate priceIds
|
||||
$seenPriceIds = [];
|
||||
foreach ($pricing as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::GENERAL_BAD_REQUEST, 'Invalid pricing entry format');
|
||||
}
|
||||
$priceId = (string) ($entry['priceId'] ?? '');
|
||||
if ($priceId === '') {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::GENERAL_BAD_REQUEST, 'Each pricing entry must include a priceId');
|
||||
}
|
||||
if (isset($seenPriceIds[$priceId])) {
|
||||
throw new \Appwrite\AppwriteException(\Appwrite\Extend\Exception::GENERAL_BAD_REQUEST, 'Duplicate priceId detected: ' . $priceId);
|
||||
}
|
||||
$seenPriceIds[$priceId] = true;
|
||||
$normalizedPricing[] = $entry;
|
||||
}
|
||||
$pricing = $normalizedPricing;
|
||||
$plan->setAttribute('pricing', $pricing);
|
||||
}
|
||||
$plan = $dbForProject->updateDocument('payments_plans', $plan->getId(), $plan);
|
||||
|
||||
@@ -2,16 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Plans;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\PaymentPlans;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Base
|
||||
@@ -36,7 +42,9 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'plans',
|
||||
name: 'list',
|
||||
description: 'List payment plans',
|
||||
description: <<<EOT
|
||||
Get a list of all payment plans configured for the project. You can use the query params to filter your results.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -45,26 +53,68 @@ class XList extends Base
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('search', '', new Text(256), 'Search term.', true)
|
||||
->param('queries', [], new PaymentPlans(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', PaymentPlans::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$filters = [];
|
||||
if ($search !== '') {
|
||||
$filters[] = Query::search('search', $search);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
$plans = $dbForProject->find('payments_plans', $filters);
|
||||
$payload = [
|
||||
'total' => count($plans),
|
||||
'plans' => array_map(fn ($d) => $d->getArrayCopy(), $plans)
|
||||
];
|
||||
$response->dynamic(new Document($payload), Response::MODEL_PAYMENT_PLAN_LIST);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$planId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('payments_plans', $planId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Plan '{$planId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$plans = $dbForProject->find('payments_plans', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('payments_plans', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'plans' => $plans,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PAYMENT_PLAN_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'providers',
|
||||
name: 'test',
|
||||
description: 'Test payments provider credentials',
|
||||
description: <<<EOT
|
||||
Test the connection to a configured payment provider.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -34,7 +34,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'providers',
|
||||
name: 'get',
|
||||
description: 'Get payments providers configuration',
|
||||
description: <<<EOT
|
||||
Get the current payment provider configuration for the project.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -43,7 +43,9 @@ class Update extends Base
|
||||
namespace: 'payments',
|
||||
group: 'providers',
|
||||
name: 'update',
|
||||
description: 'Configure payments providers',
|
||||
description: <<<EOT
|
||||
Configure payment providers for the project. Currently supports Stripe.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -44,7 +44,9 @@ class Cancel extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'cancel',
|
||||
description: 'Cancel a subscription',
|
||||
description: <<<EOT
|
||||
Cancel a subscription. By default, cancellation takes effect at the end of the current billing period.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -43,7 +43,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'create',
|
||||
description: 'Create a subscription',
|
||||
description: <<<EOT
|
||||
Create a new subscription for a user or team. This initiates a checkout session with the configured payment provider and returns a checkout URL for payment completion.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -41,7 +41,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'get',
|
||||
description: 'Get a subscription',
|
||||
description: <<<EOT
|
||||
Get a subscription by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Subscriptions;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Payments\Provider\StripeAdapter;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
@@ -39,7 +38,9 @@ class Portal extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'createPortal',
|
||||
description: 'Create a billing portal session',
|
||||
description: <<<EOT
|
||||
Get a URL to the payment provider's customer portal where users can manage their subscription and payment methods.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Subscriptions;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Payments\Provider\ProviderSubscriptionRef;
|
||||
use Appwrite\Payments\Provider\Registry;
|
||||
use Appwrite\Payments\Provider\StripeAdapter;
|
||||
use Appwrite\Payments\Provider\ProviderSubscriptionRef;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -40,7 +40,9 @@ class PreviewUpgrade extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'previewUpgrade',
|
||||
description: 'Preview a subscription upgrade with proration details',
|
||||
description: <<<EOT
|
||||
Preview the cost of upgrading or downgrading a subscription to a different plan.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -43,7 +43,9 @@ class Resume extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'resume',
|
||||
description: 'Resume a subscription',
|
||||
description: <<<EOT
|
||||
Resume a previously canceled subscription if it hasn't expired yet.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -45,7 +45,9 @@ class Update extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'update',
|
||||
description: 'Update a subscription',
|
||||
description: <<<EOT
|
||||
Update a subscription to change the associated plan. This may trigger prorated charges or credits depending on the provider configuration.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -2,16 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Subscriptions;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\PaymentSubscriptions;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Base
|
||||
@@ -36,7 +42,9 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'subscriptions',
|
||||
name: 'list',
|
||||
description: 'List subscriptions',
|
||||
description: <<<EOT
|
||||
Get a list of all the project's subscriptions. You can use the query params to filter your results.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -45,32 +53,64 @@ class XList extends Base
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('actorType', '', new Text(16, 0), 'Filter by actor type', true)
|
||||
->param('actorId', '', new Text(128, 0), 'Filter by actor ID', true)
|
||||
->param('status', '', new Text(32, 0), 'Filter by status', true)
|
||||
->param('queries', [], new PaymentSubscriptions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', PaymentSubscriptions::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $actorType,
|
||||
string $actorId,
|
||||
string $status,
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$filters = [];
|
||||
if ($actorType !== '') {
|
||||
$filters[] = Query::equal('actorType', [$actorType]);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
if ($actorId !== '') {
|
||||
$filters[] = Query::equal('actorId', [$actorId]);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
if ($status !== '') {
|
||||
$filters[] = Query::equal('status', [$status]);
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$subscriptionId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('payments_subscriptions', $subscriptionId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscription '{$subscriptionId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$subscriptions = $dbForProject->find('payments_subscriptions', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('payments_subscriptions', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
$subscriptions = $dbForProject->find('payments_subscriptions', $filters);
|
||||
|
||||
// Collect unique plan IDs and fetch plans
|
||||
$plansById = [];
|
||||
@@ -96,7 +136,7 @@ class XList extends Base
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'subscriptions' => $subscriptions,
|
||||
'total' => count($subscriptions),
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PAYMENT_SUBSCRIPTION_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'usage',
|
||||
name: 'report',
|
||||
description: 'Report usage for subscription',
|
||||
description: <<<EOT
|
||||
Report usage for a metered feature on a subscription.
|
||||
EOT,
|
||||
auth: [AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Http\Usage\Events;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\PaymentUsageEvents;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Base
|
||||
@@ -35,17 +42,22 @@ class XList extends Base
|
||||
namespace: 'payments',
|
||||
group: 'usage',
|
||||
name: 'listEvents',
|
||||
description: 'List usage events',
|
||||
description: <<<EOT
|
||||
Get a list of usage events for a subscription.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_ANY,
|
||||
model: Response::MODEL_PAYMENT_USAGE_EVENT_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('subscriptionId', '', new Text(128, 0), 'Filter by subscription ID', true)
|
||||
->param('featureId', '', new Text(128, 0), 'Filter by feature ID', true)
|
||||
->param('subscriptionId', '', new Text(128, 0), 'Filter by subscription ID.', true)
|
||||
->param('featureId', '', new Text(128, 0), 'Filter by feature ID.', true)
|
||||
->param('queries', [], new PaymentUsageEvents(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', PaymentUsageEvents::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
@@ -54,20 +66,67 @@ class XList extends Base
|
||||
public function action(
|
||||
string $subscriptionId,
|
||||
string $featureId,
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$filters = [];
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Add filters from explicit params
|
||||
if ($subscriptionId !== '') {
|
||||
$filters[] = Query::equal('subscriptionId', [$subscriptionId]);
|
||||
$queries[] = Query::equal('subscriptionId', [$subscriptionId]);
|
||||
}
|
||||
if ($featureId !== '') {
|
||||
$filters[] = Query::equal('featureId', [$featureId]);
|
||||
$queries[] = Query::equal('featureId', [$featureId]);
|
||||
}
|
||||
$list = $dbForProject->find('payments_usage_events', $filters);
|
||||
$response->json([
|
||||
'total' => count($list),
|
||||
'events' => array_map(fn ($d) => $d->getArrayCopy(), $list)
|
||||
]);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$eventId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('payments_usage_events', $eventId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Usage event '{$eventId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$events = $dbForProject->find('payments_usage_events', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('payments_usage_events', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'events' => $events,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PAYMENT_USAGE_EVENT_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ class Get extends Base
|
||||
namespace: 'payments',
|
||||
group: 'usage',
|
||||
name: 'get',
|
||||
description: 'Get usage summary for subscription',
|
||||
description: <<<EOT
|
||||
Get usage summary for a subscription.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -38,7 +38,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'usage',
|
||||
name: 'reconcile',
|
||||
description: 'Trigger usage reconciliation',
|
||||
description: <<<EOT
|
||||
Reconcile usage events with the payment provider.
|
||||
EOT,
|
||||
auth: [AuthType::KEY, AuthType::ADMIN],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -36,7 +36,9 @@ class Create extends Base
|
||||
namespace: 'payments',
|
||||
group: 'providers',
|
||||
name: 'webhook',
|
||||
description: 'Handle provider webhook',
|
||||
description: <<<EOT
|
||||
Handle incoming webhooks from payment providers. This endpoint is called by the provider to notify of subscription and payment events.
|
||||
EOT,
|
||||
auth: [],
|
||||
responses: []
|
||||
))
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Payments\Http\ActorFeatures\Get as ActorFeaturesGet;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Features\Create as FeaturesCreate;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Features\Delete as FeaturesDelete;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Features\Get as FeaturesGet;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Features\Update as FeaturesUpdate;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Features\XList as FeaturesList;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Invoices\XList as InvoicesList;
|
||||
use Appwrite\Platform\Modules\Payments\Http\PlanFeatures\Assign as PlanFeaturesAssign;
|
||||
use Appwrite\Platform\Modules\Payments\Http\PlanFeatures\Remove as PlanFeaturesRemove;
|
||||
use Appwrite\Platform\Modules\Payments\Http\PlanFeatures\XList as PlanFeaturesList;
|
||||
@@ -26,13 +28,11 @@ use Appwrite\Platform\Modules\Payments\Http\Subscriptions\PreviewUpgrade as Subs
|
||||
use Appwrite\Platform\Modules\Payments\Http\Subscriptions\Resume as SubscriptionsResume;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Subscriptions\Update as SubscriptionsUpdate;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Subscriptions\XList as SubscriptionsList;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Invoices\XList as InvoicesList;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Usage\Create as UsageCreate;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Usage\Events\XList as UsageEventsList;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Usage\Get as UsageGet;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Usage\Reconcile\Create as UsageReconcile;
|
||||
use Appwrite\Platform\Modules\Payments\Http\Webhooks\Provider\Create as WebhookProviderCreate;
|
||||
use Appwrite\Platform\Modules\Payments\Http\ActorFeatures\Get as ActorFeaturesGet;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
class FeatureTier extends Validator
|
||||
{
|
||||
private string $description = '';
|
||||
|
||||
/**
|
||||
* Get description of the validation error
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates this validator expects an array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a feature tier entry for metered billing
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
// Must be array-like
|
||||
if (!\is_array($value)) {
|
||||
$this->description = 'Feature tier must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate upTo - should be present (integer or 'inf')
|
||||
if (\array_key_exists('upTo', $value)) {
|
||||
if (!\is_int($value['upTo']) && $value['upTo'] !== 'inf' && $value['upTo'] !== null) {
|
||||
$this->description = "Key 'upTo' must be an integer, 'inf', or null";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate unitAmount - should be present (numeric)
|
||||
if (\array_key_exists('unitAmount', $value)) {
|
||||
if (!\is_int($value['unitAmount']) && !\is_float($value['unitAmount'])) {
|
||||
$this->description = "Key 'unitAmount' must be a numeric value";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate flatAmount - optional (numeric)
|
||||
if (\array_key_exists('flatAmount', $value)) {
|
||||
if (!\is_int($value['flatAmount']) && !\is_float($value['flatAmount']) && $value['flatAmount'] !== null) {
|
||||
$this->description = "Key 'flatAmount' must be a numeric value or null";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate firstUnit - optional (integer)
|
||||
if (\array_key_exists('firstUnit', $value)) {
|
||||
if (!\is_int($value['firstUnit'])) {
|
||||
$this->description = "Key 'firstUnit' must be an integer";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lastUnit - optional (integer or null for infinity)
|
||||
if (\array_key_exists('lastUnit', $value)) {
|
||||
if (!\is_int($value['lastUnit']) && $value['lastUnit'] !== null) {
|
||||
$this->description = "Key 'lastUnit' must be an integer or null";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of this validator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_OBJECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
class PricingEntry extends Validator
|
||||
{
|
||||
private string $description = '';
|
||||
|
||||
/**
|
||||
* Get description of the validation error
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates this validator expects an array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a pricing entry
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
// Must be array-like
|
||||
if (!\is_array($value)) {
|
||||
$this->description = 'Pricing entry must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate priceId - required, non-empty string
|
||||
if (!\array_key_exists('priceId', $value)) {
|
||||
$this->description = 'Missing required key: priceId';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_string($value['priceId']) || \trim($value['priceId']) === '') {
|
||||
$this->description = "Key 'priceId' must be a non-empty string";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate amount - should be present (integer)
|
||||
if (\array_key_exists('amount', $value)) {
|
||||
if (!\is_int($value['amount']) && !\is_float($value['amount'])) {
|
||||
$this->description = "Key 'amount' must be a numeric value";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate currency - should be present (string)
|
||||
if (\array_key_exists('currency', $value)) {
|
||||
if (!\is_string($value['currency'])) {
|
||||
$this->description = "Key 'currency' must be a string";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate interval - should be present (string)
|
||||
if (\array_key_exists('interval', $value)) {
|
||||
if (!\is_string($value['interval'])) {
|
||||
$this->description = "Key 'interval' must be a string";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of this validator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_OBJECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Payments\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
class ProviderConfig extends Validator
|
||||
{
|
||||
private string $description = '';
|
||||
|
||||
/**
|
||||
* Get description of the validation error
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates this validator expects an array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a provider configuration object
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
// Must be array-like
|
||||
if (!\is_array($value)) {
|
||||
$this->description = 'Provider configuration must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have 'providers' key
|
||||
if (!\array_key_exists('providers', $value)) {
|
||||
$this->description = "Missing required key: providers";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_array($value['providers'])) {
|
||||
$this->description = "Key 'providers' must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each provider entry
|
||||
foreach ($value['providers'] as $providerId => $providerConfig) {
|
||||
if (!\is_string($providerId) || \trim($providerId) === '') {
|
||||
$this->description = 'Provider ID must be a non-empty string';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_array($providerConfig)) {
|
||||
$this->description = "Provider '{$providerId}' configuration must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate secretKey - required, non-empty string
|
||||
if (!\array_key_exists('secretKey', $providerConfig)) {
|
||||
$this->description = "Missing required key 'secretKey' for provider '{$providerId}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_string($providerConfig['secretKey']) || \trim($providerConfig['secretKey']) === '') {
|
||||
$this->description = "Key 'secretKey' for provider '{$providerId}' must be a non-empty string";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate optional 'enabled' key if present
|
||||
if (\array_key_exists('enabled', $providerConfig)) {
|
||||
if (!\is_bool($providerConfig['enabled'])) {
|
||||
$this->description = "Key 'enabled' for provider '{$providerId}' must be a boolean";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate optional 'defaults' key if present
|
||||
if (\array_key_exists('defaults', $providerConfig)) {
|
||||
if (!\is_array($providerConfig['defaults'])) {
|
||||
$this->description = "Key 'defaults' for provider '{$providerId}' must be an array";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate optional top-level 'enabled' key if present
|
||||
if (\array_key_exists('enabled', $value)) {
|
||||
if (!\is_bool($value['enabled'])) {
|
||||
$this->description = "Key 'enabled' must be a boolean";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate optional top-level 'defaults' key if present
|
||||
if (\array_key_exists('defaults', $value)) {
|
||||
if (!\is_array($value['defaults'])) {
|
||||
$this->description = "Key 'defaults' must be an array";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of this validator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_OBJECT;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\System\System;
|
||||
|
||||
class SchedulePaymentsUsage extends Action
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class PaymentFeatures extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'featureId',
|
||||
'name',
|
||||
'type',
|
||||
'status'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('payments_features', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class PaymentPlanFeatures extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'planId',
|
||||
'featureId',
|
||||
'enabled',
|
||||
'type'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('payments_plan_features', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class PaymentPlans extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'planId',
|
||||
'name',
|
||||
'status',
|
||||
'isDefault',
|
||||
'isFree'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('payments_plans', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class PaymentSubscriptions extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'subscriptionId',
|
||||
'actorType',
|
||||
'actorId',
|
||||
'planId',
|
||||
'status',
|
||||
'cancelAtPeriodEnd'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('payments_subscriptions', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class PaymentUsageEvents extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'subscriptionId',
|
||||
'featureId',
|
||||
'quantity'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('payments_usage_events', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -371,6 +371,12 @@ class Response extends SwooleResponse
|
||||
public const MODEL_PAYMENT_SUBSCRIPTION_LIST = 'paymentSubscriptionList';
|
||||
public const MODEL_PAYMENT_PLAN_LIST = 'paymentPlanList';
|
||||
public const MODEL_PAYMENT_FEATURE_LIST = 'paymentFeatureList';
|
||||
public const MODEL_PAYMENT_PLAN_FEATURE = 'paymentPlanFeature';
|
||||
public const MODEL_PAYMENT_PLAN_FEATURE_LIST = 'paymentPlanFeatureList';
|
||||
public const MODEL_PAYMENT_INVOICE = 'paymentInvoice';
|
||||
public const MODEL_PAYMENT_INVOICE_LIST = 'paymentInvoiceList';
|
||||
public const MODEL_PAYMENT_USAGE_EVENT = 'paymentUsageEvent';
|
||||
public const MODEL_PAYMENT_USAGE_EVENT_LIST = 'paymentUsageEventList';
|
||||
public const MODEL_PAYMENT_PROVIDER_CONFIG = 'paymentProviderConfig';
|
||||
public const MODEL_WEBHOOK = 'webhook';
|
||||
public const MODEL_WEBHOOK_LIST = 'webhookList';
|
||||
@@ -572,9 +578,15 @@ class Response extends SwooleResponse
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentFeature())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentSubscription())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentProviderConfig())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentPlanFeature())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentInvoice())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\PaymentUsageEvent())
|
||||
->setModel(new BaseList('Payment Subscription List', self::MODEL_PAYMENT_SUBSCRIPTION_LIST, 'subscriptions', self::MODEL_PAYMENT_SUBSCRIPTION, true, false))
|
||||
->setModel(new BaseList('Payment Plan List', self::MODEL_PAYMENT_PLAN_LIST, 'plans', self::MODEL_PAYMENT_PLAN, true, false))
|
||||
->setModel(new BaseList('Payment Feature List', self::MODEL_PAYMENT_FEATURE_LIST, 'features', self::MODEL_PAYMENT_FEATURE, true, false))
|
||||
->setModel(new BaseList('Payment Plan Feature List', self::MODEL_PAYMENT_PLAN_FEATURE_LIST, 'features', self::MODEL_PAYMENT_PLAN_FEATURE, true, false))
|
||||
->setModel(new BaseList('Payment Invoice List', self::MODEL_PAYMENT_INVOICE_LIST, 'invoices', self::MODEL_PAYMENT_INVOICE, true, false))
|
||||
->setModel(new BaseList('Payment Usage Event List', self::MODEL_PAYMENT_USAGE_EVENT_LIST, 'events', self::MODEL_PAYMENT_USAGE_EVENT, true, false))
|
||||
->setModel(new Installation())
|
||||
->setModel(new ProviderRepository())
|
||||
->setModel(new ProviderRepositoryFramework())
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class PaymentInvoice extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('invoiceId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Invoice ID.',
|
||||
'default' => '',
|
||||
'example' => 'inv_1234567890',
|
||||
])
|
||||
->addRule('subscriptionId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Subscription ID.',
|
||||
'default' => '',
|
||||
'example' => 'sub_1234567890',
|
||||
])
|
||||
->addRule('amount', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Invoice amount in cents.',
|
||||
'default' => 0,
|
||||
'example' => 2999,
|
||||
])
|
||||
->addRule('currency', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Currency code.',
|
||||
'default' => '',
|
||||
'example' => 'usd',
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Invoice status.',
|
||||
'default' => '',
|
||||
'example' => 'paid',
|
||||
])
|
||||
->addRule('createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Invoice creation date.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('paidAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Invoice payment date.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('invoiceUrl', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'URL to view the invoice.',
|
||||
'default' => '',
|
||||
'example' => 'https://invoice.stripe.com/i/acct_123/inv_456',
|
||||
])
|
||||
->addRule('metadata', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'Additional metadata.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'PaymentInvoice';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PAYMENT_INVOICE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class PaymentPlanFeature extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('planId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Plan ID.',
|
||||
'default' => '',
|
||||
'example' => 'pro',
|
||||
])
|
||||
->addRule('featureId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Feature ID.',
|
||||
'default' => '',
|
||||
'example' => 'seats',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Feature type (boolean, metered).',
|
||||
'default' => 'boolean',
|
||||
'example' => 'metered',
|
||||
])
|
||||
->addRule('enabled', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether the feature is enabled for this plan.',
|
||||
'default' => true,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('currency', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Currency code for metered features.',
|
||||
'default' => '',
|
||||
'example' => 'usd',
|
||||
])
|
||||
->addRule('interval', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Billing interval for metered features.',
|
||||
'default' => '',
|
||||
'example' => 'month',
|
||||
])
|
||||
->addRule('includedUnits', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of units included in the plan.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('tiersMode', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Pricing tiers mode (graduated or volume).',
|
||||
'default' => '',
|
||||
'example' => 'graduated',
|
||||
])
|
||||
->addRule('tiers', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'Pricing tiers configuration.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
])
|
||||
->addRule('usageCap', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Maximum usage cap.',
|
||||
'default' => null,
|
||||
'example' => 1000,
|
||||
])
|
||||
->addRule('overagePrice', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Price per unit for overage usage.',
|
||||
'default' => null,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('providers', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'Provider-specific metadata.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
])
|
||||
->addRule('metadata', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'Additional metadata.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'PaymentPlanFeature';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PAYMENT_PLAN_FEATURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class PaymentUsageEvent extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('subscriptionId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Subscription ID.',
|
||||
'default' => '',
|
||||
'example' => 'sub_abc123',
|
||||
])
|
||||
->addRule('actorType', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Actor type (user or team).',
|
||||
'default' => 'user',
|
||||
'example' => 'user',
|
||||
])
|
||||
->addRule('actorId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Actor ID.',
|
||||
'default' => '',
|
||||
'example' => 'user_123',
|
||||
])
|
||||
->addRule('planId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Plan ID.',
|
||||
'default' => '',
|
||||
'example' => 'pro',
|
||||
])
|
||||
->addRule('featureId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Feature ID.',
|
||||
'default' => '',
|
||||
'example' => 'api_calls',
|
||||
])
|
||||
->addRule('quantity', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Usage quantity.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('timestamp', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Event timestamp.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('providerSyncState', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider sync state.',
|
||||
'default' => 'pending',
|
||||
'example' => 'synced',
|
||||
])
|
||||
->addRule('providerEventId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider event ID.',
|
||||
'default' => '',
|
||||
'example' => 'evt_123',
|
||||
])
|
||||
->addRule('metadata', [
|
||||
'type' => self::TYPE_JSON,
|
||||
'description' => 'Additional metadata.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'PaymentUsageEvent';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PAYMENT_USAGE_EVENT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Payments;
|
||||
|
||||
use Appwrite\Tests\Async;
|
||||
use Tests\E2E\Client;
|
||||
|
||||
trait PaymentsBase
|
||||
{
|
||||
use Async;
|
||||
|
||||
protected function setupPlan(array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'planId' => 'test-plan-' . uniqid(),
|
||||
'name' => 'Test Plan',
|
||||
'description' => 'Test plan description',
|
||||
'isDefault' => false,
|
||||
'isFree' => false,
|
||||
'pricing' => []
|
||||
];
|
||||
$params = array_merge($defaults, $params);
|
||||
|
||||
$plan = $this->client->call(Client::METHOD_POST, '/payments/plans', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), $params);
|
||||
|
||||
$this->assertEquals(201, $plan['headers']['status-code'], 'Setup plan failed with status code: ' . $plan['headers']['status-code'] . ' and response: ' . json_encode($plan['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
return $plan['body'];
|
||||
}
|
||||
|
||||
protected function cleanupPlan(string $planId): void
|
||||
{
|
||||
$plan = $this->client->call(Client::METHOD_DELETE, '/payments/plans/' . $planId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $plan['headers']['status-code']);
|
||||
}
|
||||
|
||||
protected function createPlan(array $params = []): array
|
||||
{
|
||||
$plan = $this->client->call(Client::METHOD_POST, '/payments/plans', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
protected function getPlan(string $planId): array
|
||||
{
|
||||
$plan = $this->client->call(Client::METHOD_GET, '/payments/plans/' . $planId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
protected function listPlans(array $params = []): array
|
||||
{
|
||||
$plans = $this->client->call(Client::METHOD_GET, '/payments/plans', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $plans;
|
||||
}
|
||||
|
||||
protected function updatePlan(string $planId, array $params = []): array
|
||||
{
|
||||
$plan = $this->client->call(Client::METHOD_PUT, '/payments/plans/' . $planId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
protected function deletePlan(string $planId): array
|
||||
{
|
||||
$plan = $this->client->call(Client::METHOD_DELETE, '/payments/plans/' . $planId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
protected function setupFeature(array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'featureId' => 'test-feature-' . uniqid(),
|
||||
'name' => 'Test Feature',
|
||||
'type' => 'boolean',
|
||||
'description' => 'Test feature description'
|
||||
];
|
||||
$params = array_merge($defaults, $params);
|
||||
|
||||
$feature = $this->client->call(Client::METHOD_POST, '/payments/features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), $params);
|
||||
|
||||
$this->assertEquals(201, $feature['headers']['status-code'], 'Setup feature failed with status code: ' . $feature['headers']['status-code'] . ' and response: ' . json_encode($feature['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
return $feature['body'];
|
||||
}
|
||||
|
||||
protected function cleanupFeature(string $featureId): void
|
||||
{
|
||||
$feature = $this->client->call(Client::METHOD_DELETE, '/payments/features/' . $featureId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $feature['headers']['status-code']);
|
||||
}
|
||||
|
||||
protected function createFeature(array $params = []): array
|
||||
{
|
||||
$feature = $this->client->call(Client::METHOD_POST, '/payments/features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
protected function getFeature(string $featureId): array
|
||||
{
|
||||
$feature = $this->client->call(Client::METHOD_GET, '/payments/features/' . $featureId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
protected function listFeatures(array $params = []): array
|
||||
{
|
||||
$features = $this->client->call(Client::METHOD_GET, '/payments/features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
protected function updateFeature(string $featureId, array $params = []): array
|
||||
{
|
||||
$feature = $this->client->call(Client::METHOD_PUT, '/payments/features/' . $featureId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
protected function deleteFeature(string $featureId): array
|
||||
{
|
||||
$feature = $this->client->call(Client::METHOD_DELETE, '/payments/features/' . $featureId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
protected function setupSubscription(string $planId, array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'actorType' => 'user',
|
||||
'actorId' => $this->getUser()['$id'],
|
||||
'planId' => $planId,
|
||||
'successUrl' => 'https://example.com/success',
|
||||
'cancelUrl' => 'https://example.com/cancel'
|
||||
];
|
||||
$params = array_merge($defaults, $params);
|
||||
|
||||
$subscription = $this->client->call(Client::METHOD_POST, '/payments/subscriptions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), $params);
|
||||
|
||||
$this->assertEquals(201, $subscription['headers']['status-code'], 'Setup subscription failed with status code: ' . $subscription['headers']['status-code'] . ' and response: ' . json_encode($subscription['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
return $subscription['body'];
|
||||
}
|
||||
|
||||
protected function cleanupSubscription(string $subscriptionId): void
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_POST, '/payments/subscriptions/' . $subscriptionId . '/cancel', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'endAtPeriodEnd' => false
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $subscription['headers']['status-code']);
|
||||
}
|
||||
|
||||
protected function createSubscription(array $params = []): array
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_POST, '/payments/subscriptions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
protected function getSubscription(string $subscriptionId): array
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_GET, '/payments/subscriptions/' . $subscriptionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
protected function listSubscriptions(array $params = []): array
|
||||
{
|
||||
$subscriptions = $this->client->call(Client::METHOD_GET, '/payments/subscriptions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
protected function updateSubscription(string $subscriptionId, array $params = []): array
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_PUT, '/payments/subscriptions/' . $subscriptionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
protected function cancelSubscription(string $subscriptionId, array $params = []): array
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_POST, '/payments/subscriptions/' . $subscriptionId . '/cancel', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
protected function resumeSubscription(string $subscriptionId): array
|
||||
{
|
||||
$subscription = $this->client->call(Client::METHOD_POST, '/payments/subscriptions/' . $subscriptionId . '/resume', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
protected function getSubscriptionPortal(string $subscriptionId, array $params = []): array
|
||||
{
|
||||
$portal = $this->client->call(Client::METHOD_GET, '/payments/subscriptions/' . $subscriptionId . '/portal', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $portal;
|
||||
}
|
||||
|
||||
protected function previewUpgrade(string $subscriptionId, array $params = []): array
|
||||
{
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/payments/subscriptions/' . $subscriptionId . '/preview-upgrade', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
protected function listInvoices(array $params = []): array
|
||||
{
|
||||
$invoices = $this->client->call(Client::METHOD_GET, '/payments/invoices', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $invoices;
|
||||
}
|
||||
|
||||
protected function getUsage(array $params = []): array
|
||||
{
|
||||
$usage = $this->client->call(Client::METHOD_GET, '/payments/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $usage;
|
||||
}
|
||||
|
||||
protected function createUsage(array $params = []): array
|
||||
{
|
||||
$usage = $this->client->call(Client::METHOD_POST, '/payments/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $usage;
|
||||
}
|
||||
|
||||
protected function listUsageEvents(array $params = []): array
|
||||
{
|
||||
$events = $this->client->call(Client::METHOD_GET, '/payments/usage/events', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
protected function getProvider(string $providerId): array
|
||||
{
|
||||
$provider = $this->client->call(Client::METHOD_GET, '/payments/providers/' . $providerId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
protected function updateProvider(string $providerId, array $params = []): array
|
||||
{
|
||||
$provider = $this->client->call(Client::METHOD_PUT, '/payments/providers/' . $providerId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
protected function assignPlanFeature(string $planId, array $params = []): array
|
||||
{
|
||||
$planFeature = $this->client->call(Client::METHOD_POST, '/payments/plans/' . $planId . '/features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $planFeature;
|
||||
}
|
||||
|
||||
protected function listPlanFeatures(string $planId, array $params = []): array
|
||||
{
|
||||
$planFeatures = $this->client->call(Client::METHOD_GET, '/payments/plans/' . $planId . '/features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $planFeatures;
|
||||
}
|
||||
|
||||
protected function removePlanFeature(string $planId, string $featureId): array
|
||||
{
|
||||
$planFeature = $this->client->call(Client::METHOD_DELETE, '/payments/plans/' . $planId . '/features/' . $featureId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
return $planFeature;
|
||||
}
|
||||
|
||||
protected function getActorFeatures(array $params = []): array
|
||||
{
|
||||
$actorFeatures = $this->client->call(Client::METHOD_GET, '/payments/actor-features', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), $params);
|
||||
|
||||
return $actorFeatures;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Payments;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class PaymentsConsoleClientTest extends Scope
|
||||
{
|
||||
use PaymentsBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
|
||||
// Add test methods for console client tests
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Payments;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class PaymentsCustomClientTest extends Scope
|
||||
{
|
||||
use PaymentsBase;
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
|
||||
// Add test methods for custom client tests
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Payments;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class PaymentsCustomServerTest extends Scope
|
||||
{
|
||||
use PaymentsBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
|
||||
// Add test methods for server-side tests
|
||||
}
|
||||
Reference in New Issue
Block a user