feat: provisioning API endpoints and service documentation
Implement the provisioning API endpoints referenced in routes/api.php and add comprehensive PHPDoc to service classes missing documentation. Provisioning API (Issue #15): - ProductApiController: ping, product listing, product lookup by SKU - EntitlementApiController: create, show, suspend, unsuspend, cancel, renew - Uncomment and activate provisioning route group with commerce.api middleware - Register commerce.api and commerce.matrix middleware aliases in Boot.php Service documentation (Issue #14): - CreditNoteService: lifecycle, FIFO ordering, state machine - RefundService: gateway orchestration, eligibility, transaction safety - SubscriptionService: lifecycle, proration, fixed-day billing periods - CouponService: sanitisation, validation, Orderable polymorphism - InvoiceService: PDF generation, storage, email delivery Fixes #14 Fixes #15 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5bce748a0f
commit
0685429c74
9 changed files with 560 additions and 15 deletions
6
Boot.php
6
Boot.php
|
|
@ -34,6 +34,7 @@ use Core\Mod\Commerce\Services\SubscriptionService;
|
|||
use Core\Mod\Commerce\Services\TaxService;
|
||||
use Core\Mod\Commerce\Services\UsageBillingService;
|
||||
use Core\Mod\Commerce\Services\WebhookRateLimiter;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
|
@ -65,6 +66,11 @@ class Boot extends ServiceProvider
|
|||
{
|
||||
$this->loadMigrationsFrom(__DIR__.'/Migrations');
|
||||
|
||||
// Register middleware aliases
|
||||
$router = $this->app->make(Router::class);
|
||||
$router->aliasMiddleware('commerce.api', Middleware\CommerceApiAuth::class);
|
||||
$router->aliasMiddleware('commerce.matrix', Middleware\CommerceMatrixGate::class);
|
||||
|
||||
// Laravel event listeners (not lifecycle events)
|
||||
Event::subscribe(ProvisionSocialHostSubscription::class);
|
||||
Event::listen(SubscriptionCreated::class, RewardAgentReferralOnSubscription::class);
|
||||
|
|
|
|||
381
Controllers/Api/EntitlementApiController.php
Normal file
381
Controllers/Api/EntitlementApiController.php
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Commerce\Controllers\Api;
|
||||
|
||||
use Core\Front\Controller;
|
||||
use Core\Mod\Commerce\Models\Subscription;
|
||||
use Core\Mod\Commerce\Services\SubscriptionService;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Core\Tenant\Services\EntitlementService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Provisioning API controller for entitlement lifecycle management.
|
||||
*
|
||||
* Provides endpoints for creating, inspecting, suspending, unsuspending,
|
||||
* cancelling, and renewing entitlements (workspace package subscriptions).
|
||||
* Authenticated via Bearer token through the CommerceApiAuth middleware.
|
||||
*
|
||||
* Entitlements represent the link between a workspace, a package, and
|
||||
* its active subscription. Each entitlement is identified by its
|
||||
* subscription ID.
|
||||
*/
|
||||
class EntitlementApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected EntitlementService $entitlements,
|
||||
protected SubscriptionService $subscriptionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Provision a new entitlement for a workspace.
|
||||
*
|
||||
* POST /api/v1/provisioning/entitlements
|
||||
*
|
||||
* Request body:
|
||||
* - workspace_id (required): The workspace to provision
|
||||
* - package_code (required): The entitlement package code
|
||||
* - billing_cycle (optional): "monthly" or "yearly" (default: "monthly")
|
||||
* - gateway (optional): Payment gateway identifier
|
||||
* - gateway_subscription_id (optional): External subscription reference
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'workspace_id' => 'required|integer|exists:workspaces,id',
|
||||
'package_code' => 'required|string',
|
||||
'billing_cycle' => 'in:monthly,yearly',
|
||||
'gateway' => 'nullable|string',
|
||||
'gateway_subscription_id' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$workspace = Workspace::findOrFail($validated['workspace_id']);
|
||||
$billingCycle = $validated['billing_cycle'] ?? 'monthly';
|
||||
|
||||
try {
|
||||
$result = DB::transaction(function () use ($workspace, $validated, $billingCycle) {
|
||||
// Provision the package entitlements
|
||||
$workspacePackage = $this->entitlements->provisionPackage(
|
||||
$workspace,
|
||||
$validated['package_code'],
|
||||
[
|
||||
'source' => 'provisioning_api',
|
||||
'gateway' => $validated['gateway'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Create the subscription record
|
||||
$subscription = $this->subscriptionService->create(
|
||||
$workspacePackage,
|
||||
$billingCycle,
|
||||
$validated['gateway'] ?? null,
|
||||
$validated['gateway_subscription_id'] ?? null
|
||||
);
|
||||
|
||||
return [
|
||||
'workspace_package' => $workspacePackage,
|
||||
'subscription' => $subscription,
|
||||
];
|
||||
});
|
||||
|
||||
Log::info('Entitlement provisioned via API', [
|
||||
'workspace_id' => $workspace->id,
|
||||
'package_code' => $validated['package_code'],
|
||||
'subscription_id' => $result['subscription']->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'id' => $result['subscription']->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'package_code' => $validated['package_code'],
|
||||
'billing_cycle' => $billingCycle,
|
||||
'status' => $result['subscription']->status,
|
||||
'current_period_start' => $result['subscription']->current_period_start?->toIso8601String(),
|
||||
'current_period_end' => $result['subscription']->current_period_end?->toIso8601String(),
|
||||
],
|
||||
'message' => 'Entitlement provisioned successfully',
|
||||
], 201);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Entitlement provisioning failed', [
|
||||
'workspace_id' => $workspace->id,
|
||||
'package_code' => $validated['package_code'],
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'provisioning_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show entitlement details by subscription ID.
|
||||
*
|
||||
* GET /api/v1/provisioning/entitlements/{id}
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$subscription = Subscription::with(['workspace', 'workspacePackage.package'])
|
||||
->find($id);
|
||||
|
||||
if (! $subscription) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => 'Entitlement not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'id' => $subscription->id,
|
||||
'workspace_id' => $subscription->workspace_id,
|
||||
'package_code' => $subscription->workspacePackage?->package?->code,
|
||||
'package_name' => $subscription->workspacePackage?->package?->name,
|
||||
'status' => $subscription->status,
|
||||
'billing_cycle' => $subscription->billing_cycle,
|
||||
'gateway' => $subscription->gateway,
|
||||
'current_period_start' => $subscription->current_period_start?->toIso8601String(),
|
||||
'current_period_end' => $subscription->current_period_end?->toIso8601String(),
|
||||
'cancelled_at' => $subscription->cancelled_at?->toIso8601String(),
|
||||
'cancellation_reason' => $subscription->cancellation_reason,
|
||||
'paused_at' => $subscription->paused_at?->toIso8601String(),
|
||||
'ended_at' => $subscription->ended_at?->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend an entitlement (pause the subscription and restrict access).
|
||||
*
|
||||
* POST /api/v1/provisioning/entitlements/{id}/suspend
|
||||
*/
|
||||
public function suspend(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$subscription = Subscription::find($id);
|
||||
|
||||
if (! $subscription) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => 'Entitlement not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (! $subscription->isActive()) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_state',
|
||||
'message' => "Cannot suspend entitlement in '{$subscription->status}' state",
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->subscriptionService->pause($subscription, force: true);
|
||||
|
||||
if ($subscription->workspace) {
|
||||
$reason = $request->get('reason', 'api_suspension');
|
||||
$this->entitlements->suspendWorkspace($subscription->workspace, $reason);
|
||||
}
|
||||
|
||||
Log::info('Entitlement suspended via API', [
|
||||
'subscription_id' => $subscription->id,
|
||||
'workspace_id' => $subscription->workspace_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Entitlement suspended successfully',
|
||||
'data' => [
|
||||
'id' => $subscription->id,
|
||||
'status' => $subscription->fresh()->status,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'suspension_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsuspend an entitlement (unpause and restore access).
|
||||
*
|
||||
* POST /api/v1/provisioning/entitlements/{id}/unsuspend
|
||||
*/
|
||||
public function unsuspend(int $id): JsonResponse
|
||||
{
|
||||
$subscription = Subscription::find($id);
|
||||
|
||||
if (! $subscription) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => 'Entitlement not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (! $subscription->isPaused()) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_state',
|
||||
'message' => "Cannot unsuspend entitlement in '{$subscription->status}' state",
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->subscriptionService->unpause($subscription);
|
||||
|
||||
if ($subscription->workspace) {
|
||||
$this->entitlements->reactivateWorkspace($subscription->workspace, 'api_unsuspension');
|
||||
}
|
||||
|
||||
Log::info('Entitlement unsuspended via API', [
|
||||
'subscription_id' => $subscription->id,
|
||||
'workspace_id' => $subscription->workspace_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Entitlement unsuspended successfully',
|
||||
'data' => [
|
||||
'id' => $subscription->id,
|
||||
'status' => $subscription->fresh()->status,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'unsuspend_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an entitlement.
|
||||
*
|
||||
* POST /api/v1/provisioning/entitlements/{id}/cancel
|
||||
*
|
||||
* Request body:
|
||||
* - reason (optional): Cancellation reason
|
||||
* - immediately (optional): Whether to cancel immediately (default: false)
|
||||
*/
|
||||
public function cancel(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'reason' => 'nullable|string|max:500',
|
||||
'immediately' => 'boolean',
|
||||
]);
|
||||
|
||||
$subscription = Subscription::find($id);
|
||||
|
||||
if (! $subscription) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => 'Entitlement not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($subscription->isCancelled() || $subscription->status === 'expired') {
|
||||
return response()->json([
|
||||
'error' => 'invalid_state',
|
||||
'message' => 'Entitlement is already cancelled or expired',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$immediately = $validated['immediately'] ?? false;
|
||||
|
||||
try {
|
||||
$this->subscriptionService->cancel(
|
||||
$subscription,
|
||||
$validated['reason'] ?? null
|
||||
);
|
||||
|
||||
if ($immediately) {
|
||||
$this->subscriptionService->expire($subscription);
|
||||
|
||||
if ($subscription->workspace && $subscription->workspacePackage?->package) {
|
||||
$this->entitlements->revokePackage(
|
||||
$subscription->workspace,
|
||||
$subscription->workspacePackage->package->code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Entitlement cancelled via API', [
|
||||
'subscription_id' => $subscription->id,
|
||||
'workspace_id' => $subscription->workspace_id,
|
||||
'immediately' => $immediately,
|
||||
]);
|
||||
|
||||
$fresh = $subscription->fresh();
|
||||
|
||||
return response()->json([
|
||||
'message' => $immediately
|
||||
? 'Entitlement cancelled immediately'
|
||||
: 'Entitlement will be cancelled at end of billing period',
|
||||
'data' => [
|
||||
'id' => $fresh->id,
|
||||
'status' => $fresh->status,
|
||||
'cancelled_at' => $fresh->cancelled_at?->toIso8601String(),
|
||||
'ended_at' => $fresh->ended_at?->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'cancellation_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew an entitlement for another billing period.
|
||||
*
|
||||
* POST /api/v1/provisioning/entitlements/{id}/renew
|
||||
*/
|
||||
public function renew(int $id): JsonResponse
|
||||
{
|
||||
$subscription = Subscription::find($id);
|
||||
|
||||
if (! $subscription) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => 'Entitlement not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (! in_array($subscription->status, ['active', 'past_due'])) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_state',
|
||||
'message' => "Cannot renew entitlement in '{$subscription->status}' state",
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$renewed = $this->subscriptionService->renew($subscription);
|
||||
|
||||
Log::info('Entitlement renewed via API', [
|
||||
'subscription_id' => $renewed->id,
|
||||
'workspace_id' => $renewed->workspace_id,
|
||||
'new_period_end' => $renewed->current_period_end,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Entitlement renewed successfully',
|
||||
'data' => [
|
||||
'id' => $renewed->id,
|
||||
'status' => $renewed->status,
|
||||
'current_period_start' => $renewed->current_period_start?->toIso8601String(),
|
||||
'current_period_end' => $renewed->current_period_end?->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'renewal_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Controllers/Api/ProductApiController.php
Normal file
92
Controllers/Api/ProductApiController.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Commerce\Controllers\Api;
|
||||
|
||||
use Core\Front\Controller;
|
||||
use Core\Mod\Commerce\Models\Product;
|
||||
use Core\Mod\Commerce\Services\ProductCatalogService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Provisioning API controller for product catalogue queries.
|
||||
*
|
||||
* Provides read-only access to the product catalogue for internal
|
||||
* services and external integrations. Authenticated via Bearer token
|
||||
* through the CommerceApiAuth middleware.
|
||||
*/
|
||||
class ProductApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected ProductCatalogService $catalogService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Health-check / connectivity ping.
|
||||
*
|
||||
* GET /api/v1/provisioning/ping
|
||||
*/
|
||||
public function ping(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
'service' => 'commerce-provisioning',
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active, visible products.
|
||||
*
|
||||
* GET /api/v1/provisioning/products
|
||||
*
|
||||
* Query parameters:
|
||||
* - category: filter by product category
|
||||
* - type: filter by product type (simple, virtual, subscription, etc.)
|
||||
* - per_page: pagination size (default 25, max 100)
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Product::query()
|
||||
->active()
|
||||
->visible()
|
||||
->orderBy('sort_order');
|
||||
|
||||
if ($category = $request->get('category')) {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
if ($type = $request->get('type')) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
$perPage = min((int) $request->get('per_page', 25), 100);
|
||||
$products = $query->paginate($perPage);
|
||||
|
||||
return response()->json($products);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a single product by SKU code.
|
||||
*
|
||||
* GET /api/v1/provisioning/products/{code}
|
||||
*/
|
||||
public function show(string $code): JsonResponse
|
||||
{
|
||||
$product = Product::where('sku', strtoupper($code))
|
||||
->active()
|
||||
->visible()
|
||||
->first();
|
||||
|
||||
if (! $product) {
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => "Product with SKU '{$code}' not found",
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json(['data' => $product]);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,19 @@ use Illuminate\Database\Eloquent\Collection;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Coupon validation and application service.
|
||||
* Coupon validation, application, and lifecycle management service.
|
||||
*
|
||||
* Provides input sanitisation, format validation, eligibility checks
|
||||
* (expiry, usage limits, package restrictions), discount calculation,
|
||||
* and usage tracking. Supports both workspace-scoped and polymorphic
|
||||
* Orderable-scoped validation for User and Workspace purchases.
|
||||
*
|
||||
* Coupon codes are normalised to uppercase and restricted to alphanumeric
|
||||
* characters, hyphens, and underscores (3-50 characters) to prevent
|
||||
* brute-force enumeration and injection attacks.
|
||||
*
|
||||
* @see Coupon
|
||||
* @see CouponValidationResult
|
||||
*/
|
||||
class CouponService
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ use Illuminate\Database\Eloquent\Collection;
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Credit note lifecycle management service.
|
||||
*
|
||||
* Handles creation, application, and voiding of credit notes for workspace
|
||||
* billing. Credit notes may originate from partial refunds, goodwill gestures,
|
||||
* or administrative adjustments and can be applied to future orders using
|
||||
* FIFO ordering (oldest credits consumed first).
|
||||
*
|
||||
* Lifecycle: draft -> issued -> (partially_used | used | void)
|
||||
*
|
||||
* @see CreditNote
|
||||
*/
|
||||
class CreditNoteService
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,7 +18,19 @@ use Illuminate\Support\Facades\Storage;
|
|||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Invoice generation and management service.
|
||||
* Invoice generation, PDF rendering, and delivery service.
|
||||
*
|
||||
* Creates invoices from completed orders and subscription renewals,
|
||||
* generates PDF documents via DomPDF, and dispatches email notifications.
|
||||
* Invoices are linked to workspaces and support configurable due dates,
|
||||
* tax calculations, and multiple line items.
|
||||
*
|
||||
* PDF storage is configurable via `commerce.pdf.storage_disk` and
|
||||
* `commerce.pdf.storage_path`. Invoices are generated on demand and
|
||||
* cached for subsequent downloads.
|
||||
*
|
||||
* @see Invoice
|
||||
* @see InvoiceItem
|
||||
*/
|
||||
class InvoiceService
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ use Illuminate\Database\Eloquent\Collection;
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Refund processing service.
|
||||
*
|
||||
* Orchestrates full and partial refunds through the appropriate payment
|
||||
* gateway, validates refund eligibility (amount limits, time windows),
|
||||
* and dispatches notifications to workspace owners. All refund operations
|
||||
* are wrapped in database transactions for consistency.
|
||||
*
|
||||
* Gateway-specific constraints (e.g. Stripe's 180-day refund window) are
|
||||
* enforced via the configurable `commerce.refunds.window_days` setting.
|
||||
*
|
||||
* @see Refund
|
||||
*/
|
||||
class RefundService
|
||||
{
|
||||
public function __construct(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ use Illuminate\Database\Eloquent\Collection;
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Subscription lifecycle management service.
|
||||
*
|
||||
* Manages the full subscription lifecycle: creation, renewal, cancellation,
|
||||
* pausing/unpausing, expiration, and plan changes (upgrades/downgrades).
|
||||
* Integrates with the EntitlementService to provision and revoke workspace
|
||||
* packages when subscriptions change state.
|
||||
*
|
||||
* Plan changes support both immediate application (with proration) and
|
||||
* deferred application at the end of the current billing period.
|
||||
*
|
||||
* Billing periods use fixed-day intervals (30 days monthly, 365 days yearly)
|
||||
* for predictable, deterministic billing calculations.
|
||||
*
|
||||
* @see Subscription
|
||||
* @see ProrationResult
|
||||
*/
|
||||
class SubscriptionService
|
||||
{
|
||||
public function __construct(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Commerce\Controllers\Api\CommerceController;
|
||||
use Core\Mod\Commerce\Controllers\Api\EntitlementApiController;
|
||||
use Core\Mod\Commerce\Controllers\Api\ProductApiController;
|
||||
use Core\Mod\Commerce\Controllers\Webhooks\BTCPayWebhookController;
|
||||
use Core\Mod\Commerce\Controllers\Webhooks\StripeWebhookController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
|
@ -35,21 +37,19 @@ Route::prefix('webhooks')->group(function () {
|
|||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Commerce Provisioning API (Bearer token auth)
|
||||
// TODO: Create ProductApiController and EntitlementApiController in
|
||||
// Mod\Commerce\Controllers\Api\ for provisioning endpoints
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Route::middleware('commerce.api')->prefix('provisioning')->group(function () {
|
||||
// Route::get('/ping', [ProductApiController::class, 'ping'])->name('api.commerce.ping');
|
||||
// Route::get('/products', [ProductApiController::class, 'index'])->name('api.commerce.products');
|
||||
// Route::get('/products/{code}', [ProductApiController::class, 'show'])->name('api.commerce.products.show');
|
||||
// Route::post('/entitlements', [EntitlementApiController::class, 'store'])->name('api.commerce.entitlements.store');
|
||||
// Route::get('/entitlements/{id}', [EntitlementApiController::class, 'show'])->name('api.commerce.entitlements.show');
|
||||
// Route::post('/entitlements/{id}/suspend', [EntitlementApiController::class, 'suspend'])->name('api.commerce.entitlements.suspend');
|
||||
// Route::post('/entitlements/{id}/unsuspend', [EntitlementApiController::class, 'unsuspend'])->name('api.commerce.entitlements.unsuspend');
|
||||
// Route::post('/entitlements/{id}/cancel', [EntitlementApiController::class, 'cancel'])->name('api.commerce.entitlements.cancel');
|
||||
// Route::post('/entitlements/{id}/renew', [EntitlementApiController::class, 'renew'])->name('api.commerce.entitlements.renew');
|
||||
// });
|
||||
Route::middleware('commerce.api')->prefix('provisioning')->group(function () {
|
||||
Route::get('/ping', [ProductApiController::class, 'ping'])->name('api.commerce.ping');
|
||||
Route::get('/products', [ProductApiController::class, 'index'])->name('api.commerce.products');
|
||||
Route::get('/products/{code}', [ProductApiController::class, 'show'])->name('api.commerce.products.show');
|
||||
Route::post('/entitlements', [EntitlementApiController::class, 'store'])->name('api.commerce.entitlements.store');
|
||||
Route::get('/entitlements/{id}', [EntitlementApiController::class, 'show'])->name('api.commerce.entitlements.show');
|
||||
Route::post('/entitlements/{id}/suspend', [EntitlementApiController::class, 'suspend'])->name('api.commerce.entitlements.suspend');
|
||||
Route::post('/entitlements/{id}/unsuspend', [EntitlementApiController::class, 'unsuspend'])->name('api.commerce.entitlements.unsuspend');
|
||||
Route::post('/entitlements/{id}/cancel', [EntitlementApiController::class, 'cancel'])->name('api.commerce.entitlements.cancel');
|
||||
Route::post('/entitlements/{id}/renew', [EntitlementApiController::class, 'renew'])->name('api.commerce.entitlements.renew');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Commerce Billing API (authenticated + verified)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue