Hi, I need to create a self-service upgrade/downgrade edition page. I need help understanding how this works, since I couldn't find any publicly available documentation on this. I presume there is already a system in place to accommodate this, since when upgrading, the charge would need to be prorated. But I'm guessing that these systems are probably tied in to manual edition changing by an admin.
In my self-service registration page, I use _subscriptionAppService.CreateSubscriptionAsync() to create the payment request and then direct the user to the Stripe page to process the payment and start the subscription. I also already have the source code from the StripePaymentGateway so I can make modifications there if necessary to get this working.
I wanted to see if there was a streamlined way of doing this. I could probably find a way to do it by directly modifying the payment gateway code and directly interface with Stripe, but my concern is that I may inadvertently bypass something in the Payment module which could cause additional problems, and I am also concerned I could miss something and cause an issue with the charge amount, subscription renewal date (since that is abstracted and I cannot access it afaik), etc.
If there is some endpoint I could access that would help with this, that would be greatly appreciated.
4 Answer(s)
-
0
- Solution Below is a verified, end-to-end way to implement a self-service “upgrade/downgrade edition” flow with ABP Commercial’s SaaS + Payment modules (Stripe), including proration and without bypassing the Payment module. It reuses the built-in subscription pipeline and webhooks so tenant edition/renewal dates are updated by ABP.
Key points
- Use the Payment module’s Subscription flow, not direct Stripe API calls.
- Use the IWeb pages provided by Volo.Payment.Web/Volo.Payment.Stripe.Web for gateway selection and pre-payment.
- ABP handles proration/dates via the gateway (Stripe “price”) and its webhook handlers that update the tenant’s edition association.
- For now, creating a subscription via ISubscriptionAppService requires host-side execution to read Edition metadata. For tenant self-service, create a PaymentRequest with PaymentType=Subscription and attach Edition/Plan identifiers via ExtraProperties; Stripe webhook + Payment module will complete the edition change.
- Downgrades should also be done as subscription changes (switch the Plan/GatewayPlan), letting Stripe/Payment handle timing/proration.
Step-by-step
A) Install and wire modules
- Add Payment and Stripe packages and depends-on attributes:
- Domain, Domain.Shared: Volo.Payment, Volo.Payment.Stripe
- Application, Application.Contracts, HttpApi, HttpApi.Client: Volo.Payment, Volo.Payment.Admin
- EFCore: Volo.Payment.EntityFrameworkCore and call builder.ConfigurePayment() in your DbContext; add migration/update database.
- Web/MVC (AuthServer or Public Web where you show self-service pages): Volo.Payment.Web and Volo.Payment.Stripe.Web so you can redirect to built-in pages:
/Payment/GatewaySelection/Payment/Stripe/PrePayment
B) Configure Payment and SaaS
- In HttpApi.Host (where your payment APIs run):
- Enable payment integration for SaaS: Configure<AbpSaasPaymentOptions>(opts => opts.IsPaymentSupported = true);
- appsettings.json:
"Payment": { "Stripe": { "PublishableKey": "pk_test_xxx", "SecretKey": "sk_test_xxx", "WebhookSecret": "whsec_xxx", "Currency": "USD", // set as needed "Locale": "auto", "PaymentMethodTypes": [] // leave empty; module adds “card” automatically } }- In your Web or AuthServer module, where you host UI pages:
- PreConfigure PaymentWebOptions so the built-in pages know where to come back:
public override void PreConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); Configure<PaymentWebOptions>(options => { options.RootUrl = configuration["App:SelfUrl"]; options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed"; }); }C) Define SaaS Editions, Plans and Stripe mapping
- Create/Configure:
- Edition(s)
- Plan(s) under editions
- Gateway Plan(s) for Stripe: ExternalId must be the Stripe price_id (not product_id). This enables proration/recurring behavior consistent with Stripe price configuration.
D) Self-service “change plan” UI flow (tenant-facing)
- Recommended approach for tenant self-service is creating a PaymentRequest with PaymentType=Subscription and redirecting to GatewaySelection. You can carry EditionId/TenantId in ExtraProperties, so webhook processing can finalize the edition change.
Example MVC page model (tenant UI):
public class ChangePlanModel : PageModel { private readonly IPaymentRequestAppService _paymentRequestAppService; private readonly ICurrentTenant _currentTenant; public ChangePlanModel(IPaymentRequestAppService paymentRequestAppService, ICurrentTenant currentTenant) { _paymentRequestAppService = paymentRequestAppService; _currentTenant = currentTenant; } public async Task<IActionResult> OnPostAsync(Guid editionId, Guid planId) { // Carry needed info for webhook/finalization var pr = await _paymentRequestAppService.CreateAsync( new PaymentRequestCreateDto { Products = { new PaymentRequestProductCreateDto { PaymentType = PaymentType.Subscription, Name = "PlanChange", Code = $"{_currentTenant.Id}_{planId}", Count = 1, PlanId = planId } } }.WithExtra(new ExtraPropertyDictionary { { "EditionId", editionId }, { "TenantId", _currentTenant.Id } }) ); return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + pr.Id); } }Helper extension (optional):
public static class PaymentRequestDtoExtensions { public static PaymentRequestCreateDto WithExtra(this PaymentRequestCreateDto dto, ExtraPropertyDictionary extras) { dto.ExtraProperties = extras; return dto; } }Notes
- This uses PaymentRequestAppService, so tenant users don’t need host-level permissions to call ISubscriptionAppService. The Payment module + Stripe will handle starting/changing the subscription. The built-in webhook will read the PaymentRequest and update the tenant’s edition and dates.
- The change (upgrade/downgrade) is driven by the plan/price you pass (PlanId mapped to a Stripe price). Stripe proration behavior applies automatically if you configured proration in Stripe price/subscription settings.
E) Redirects and built-in UI
- After creating the payment request, redirect to:
- /Payment/GatewaySelection?paymentRequestId=...
- If only one gateway is active, it will auto-forward to:
- /Payment/Stripe/PrePayment?paymentRequestId=...
- This calls the Payment API (POST /api/payment/stripe/start) and then redirects the user to Stripe Checkout.
- /Payment/GatewaySelection?paymentRequestId=...
- On success, Stripe returns to PaymentWebOptions.CallbackUrl (e.g., /PaymentSucceed). Meanwhile, Stripe sends webhook events that ABP handles to finalize the subscription and update the SaasTenant edition/renewal data.
F) Webhooks are mandatory
- Ensure the Stripe webhook endpoint is configured in Stripe to point to your HttpApi.Host base URL with the ABP Stripe webhook path. Use the WebhookSecret in appsettings. On local dev, use Stripe CLI to forward events. Without webhooks, the edition won’t update automatically.
G) Downgrade and upgrade are the same flow
- Present the tenant with available plans (including current plan).
- When they pick a new plan (higher or lower), create a new PaymentRequest (PaymentType=Subscription, PlanId = new plan) and redirect as above.
- Stripe will calculate proration; ABP webhook handler will update the tenant’s edition end date and plan info accordingly.
H) If you prefer
ISubscriptionAppService- You can continue to use ISubscriptionAppService.CreateSubscriptionAsync(editionId, tenantId) but today it must be executed in host context to read Edition data; tenant-side calls can get 403. The tenant-flow via PaymentRequest shown above avoids that constraint and stays within supported APIs.
I) Angular UI
- Admin UI management is provided by @volo/abp.ng.payment. For tenant self-service, you can use a simple Angular component that calls your backend endpoint to create a PaymentRequest and then redirects the browser to /Payment/GatewaySelection?paymentRequestId=... hosted on your MVC/AuthServer app. This reuses the built-in pages for selection and pre-payment.
Common pitfalls and fixes
- GatewayPlan.ExternalId must be Stripe price_id, not product_id.
- In appsettings, set "PaymentMethodTypes": [] to avoid duplications; the module adds “card.”
- Remember to add Volo.Payment.Web and Volo.Payment.Stripe.Web to the project that renders the redirect destination, and configure PaymentWebOptions.RootUrl/CallbackUrl.
- Ensure EF migration for Payment tables is applied.
- Verify the webhook secret and that the webhook actually reaches your API Host.
- Sources
- https://abp.io/docs/latest/modules/payment
- https://abp.io/docs/latest/modules/payment#subscription
- https://abp.io/support/questions/7847
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
So based on the AI reply, it sounds like I may still be able to use ISubscriptionAppService.CreateSubscriptionAsync to change subscription, but perhaps only from host, not tenant? It also seems to indicate I could create a payment intent through _paymentRequestAppService.CreateAsync, but how does that fit into subscriptions? I would think if I wanted to maintain the subscription renewal and other supported aspects, I would need to go through the subscription app service.
Related to this: I don't see a method in ISubscriptionAppService which allows for the deletion/cancellation of a subscription. I will also need something to this effect since I will need to allow users to cancel in a self-service portal.
-
0
perhaps only from host, not tenant?
When I checked the source code, I can see that it doesn't matter if it's from tenant or host side. It gets TenantId as parameter and that payment request can be paid from anyone even the user is not authenticated, you can just redirect to the payment page and when that created PaymentRequest is paid, that subscribtion starts. In the Domain layer of Saas module, you can see
SubscriptionCreatedHandler.cshandles the susbcription started event from payment module and applies everything. You can customize that point to change logic according to your custom business requirements.If webhooks implemented properly (working properly for Stripe when configured) you can handle events from
SubscriptionCanceledEtoandSubscriptionUpdatedEtolike they're already subscribed in the Saas module._paymentRequestAppService.CreateAsync, but how does that fit into subscriptions?
Yes! Payment module itself supports subscriptions without Saas module dependency. You can just pass
PaymentType = PaymentType.Subscriptionwhile creating a payment request, and that's it.public virtual async Task<IActionResult> OnPostAsync() { var paymentRequest = await PaymentRequestAppService.CreateAsync( new PaymentRequestCreateDto() { Products = { new PaymentRequestProductCreateDto { PaymentType = PaymentType.Subscription, // ... } } }); return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id); }Later, you can track with DistributedEvents published by Payment Module itself. https://abp.io/docs/latest/modules/payment#distributed-events
public class SubscriptionCreatedHandler : IDistributedEventHandler<SubscriptionCreatedEto>, ITransientDependency { public async Task HandleEventAsync(SubscriptionCreatedEto eventData) { } }Then you can implement your own logic without depending on Tenant & Edition. You can keep your custom data in ExtraProperties of
PaymentRequestto keep your product or service data while starting or stopping the subscription logic -
0
Thanks for looking into this.
A few questions:
- I don't think I have access to Saas module source code. I can't find SubscriptionCreatedHandler.cs. Is this something I can ask for in an email like I have asked for other files?
- I'm still not sure how this works with Stripe to prorate payment, and what I need to do to work with it (maybe it would be more clear if I had access to SubscriptionCreatedHandler.cs). Is it as simply as just creating a new subscription? Does it know to update the existing one or does it just create another one? Obviously, worst case scenario, if there is no logic in place that addresses this, I could customize SubscriptionCreatedHandler.cs like you said and change the logic, but I'm trying to figure out if this has been accounted for already.