improve the code a bit

This commit is contained in:
Atharva Deosthale
2026-01-06 23:37:15 +05:30
parent 9786b4f6cd
commit c187efadb4
49 changed files with 1595 additions and 169 deletions
@@ -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);
}
}
+12
View File
@@ -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
}