Add Subscriptions - #6
MikeAlhayek wants to merge 177 commits into
Conversation
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
Fixes three defects reported for the Transactions module and Pay Later: - Register the 'Transaction' YesSql collection in StoreCollectionOptions so the Transaction_Document table is provisioned (no such table error). - Move the reminder service, background task, reminder settings driver, and reminder settings menu into a new opt-in 'Transaction Reminders' feature (CrestApps.OrchardCore.Transactions.Notification) that depends on OrchardCore.Notifications, so INotificationService resolves only when the feature is enabled. AdminController now treats the reminder service as optional and hides the Send reminder action when the feature is off. - Add a new CrestApps.OrchardCore.Commerce module that owns the shared Commerce top-level admin menu and its icon (enabled by dependency only). Transactions and Taxation now depend on it and contribute children under the shared node, so the menu icon renders consistently. Documents the Commerce and Transactions modules and updates the changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p 5.3 - Replace the chevron breadcrumb step header with a horizontal line-with-circles stepper that visualizes progress: completed steps are green (with a check), the current step is highlighted with a warning color, and upcoming steps are muted. Uses native Bootstrap 5.3 utility and contextual classes with minimal custom CSS (only circle sizing and the connector line). - Rebuild the payment-step invoice as a standard Bootstrap table using contextual table colors (table-light/secondary/active) and right-aligned amounts, removing the grid-offset layout so it relies on native Bootstrap classes with no custom CSS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Transactions: - Introduce a registrable transaction source registry (TransactionSource, TransactionSourceOptions, AddTransactionSource) in Transactions.Core so modules register a friendly, localizable source name. - Turn the admin report Source filter into a dropdown fed from registered sources, and show the source display name in the table. - Remove the "Total outstanding" alert (and its full-scan query) from the admin report. Pay Later: - Register its "pay-later" source with a localized display name. Receipts: - Print only the receipt: the print stylesheet now suppresses the surrounding page chrome (site header and admin menu) instead of only the buttons. Docs: - Document the registrable source mechanism and the print behavior; update the 3.0.0 changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a tests My Transactions: - Give the customer statement the same toolbar as the admin report: a search bar, a status filter dropdown, and a pager. Removed the old All/Outstanding toggle and the outstanding-total alert for consistency. - Extract the shared status-filter-to-query and toolbar-item logic into TransactionStatusFilterExtensions, used by both controllers. Subscriptions checkout: - Always show the chosen payment method on the payment step, even when only one method exists (previously submitted through a hidden field and never shown). Tests: - Disable test-collection parallelization. Json.Schema's global SchemaRegistry is not thread-safe, so building schemas from parallel collections intermittently corrupted it and failed CI. Serializing collections removes the race with negligible cost (test execution 44s -> 52s). Docs: - Update transactions.md and the 3.0.0 changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The "My Subscriptions" subscriber dashboard now pages both the Payments and Subscriptions lists, each with its own independent pager so navigating one list never disturbs the other. Payments are paged at the database level and remain ordered by payment date (latest first); subscriptions are flattened from their sessions, sorted by start date (latest first), and paged in memory. Each pager uses a distinct query key (invoicesPage / subscriptionsPage) via the Pager shape's PagerId, and the page size follows the site-wide pager option. Also refresh the ecommerce planning doc to reflect the committed Transactions, Receipts, and Commerce infrastructure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@davidpuplava @wAsnk The Subscriptions module has been a long time coming! I apologize for the delay — other modules ended up taking priority and, as a result, this one was put on the back burner for a while. You may have moved on, no longer need a Subscriptions module, or even built your own solution by now. I’m not sure what you’ve ended up doing, but I wanted to let you know that I’ve been actively working on this and have made quite a bit of progress. The module has evolved significantly from where we started, and it now provides a foundation that could support building a much broader e-commerce solution on top of it. This PR currently includes the following modules:
If you’ve moved on from this or no longer have a need for it, I completely understand. But if you’re still interested, I’d really appreciate it if you could take some time to test it out and share your feedback. I’ve documented everything in the documentation site included with this PR, so there should be enough information to understand how the pieces fit together and to get the project running. If you do have a chance to try it, please let me know if you run into any issues, have questions, or — even better — have suggestions for how it could be improved. I’m particularly interested in feedback on the overall architecture and whether the modules provide a good foundation for future development. At this point, I think the structure is flexible enough that we could eventually build a full enterprise-level commerce solution on top of these modules. That’s probably a discussion for another day, but it’s one of the directions this work could support. Either way, I’d really appreciate your feedback. It would be helpful to know whether this is still something you’re interested in so I can prioritize my time accordingly. |
|
Hi @MikeAlhayek - I'm still interested in the subscription feature and I'll take a look! Thanks for picking it back up and I'll provide feedback when I get a chance to test. |
Refactor existing modules and add reusable contracts so a future e-commerce domain (Customers/Orders/Carts/Storefront) can be built without introducing further breaking changes. - G1 Customers: add Customers.Abstractions/Core with a customer and guest-ownership seam and boundary tests. - G2 Currency ownership: a product now owns its currency. The single fallback lives in DefaultProductSnapshotResolver; pricing, taxation, Stripe price sync and subscription billing all derive currency via IProductSnapshotResolver, fail closed on a blank product currency and never apply implicit FX. TaxService validates item/context currency before dispatching to any determination provider. - G3 Transactions: settlement and concurrency hardening. - G4 Financial documents: interface-only Commerce.Abstractions seam (IFinancialDocumentPolicy / IFinancialDocumentNumberGenerator) with a receipts-only default policy and no speculative number generator. - G5 Commerce boundary: Commerce stays a thin orchestration shell; CommerceModuleBoundaryTests fail the build if it takes on persistence, indexes or migrations, or if Commerce.Abstractions references domain persistence or providers. Docs updated (products, subscriptions, commerce, transactions, changelog). Full suite: 2246 tests pass; docs site builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify Stripe account connection to a single streamlined flow: paste the secret key and click Connect. The app verifies the key against Stripe, resolves the account id, and auto-provisions the webhook (capturing its signing secret). A Disconnect button (with confirmation) removes the provisioned webhook and clears credentials. An optional manual webhook signing secret supports local development via the Stripe CLI. Remove the platform OAuth machinery (connection mode, client id/redirect URI setup) so there is no per-install technical setup and no hosted broker. Keep the independent Stripe workflow events, including Request Failed which now also fires on key-verification and webhook-provisioning failures. Add an explicit OrchardCore.Settings package reference and update the payments documentation and 3.0.0 changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce CrestApps.OrchardCore.Wizard: a scenario-agnostic multi-step wizard engine usable via code (definition/handler/display-driver) and via content items (WizardPart modeled on Flows BagPart) with settings, completion policy, per-instance authentication, and workflow events. Also fixes the CI build break by passing TestContext.Current.CancellationToken in DefaultCustomerContactResolverTests (xUnit1051). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing top-level WizardFlow.Edit, WizardFlow.Confirmation and WizardFlowContentItem shape templates required by the content-driven wizard UI, and serialize authored step content with JsonSerializer.Serialize instead of JsonObject.ToJsonString to avoid a MissingMethodException that silently dropped all steps during session activation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
Implements the commerce suite review plan: a provider-neutral checkout engine with durable payment attempts, recurring payment routing for Stripe and Pay Later, durable subscription agreements with a locked and idempotent lifecycle, entitlements, tenant provisioning as a durable job, coupons and discounts applied before tax, and free trials. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buying a plan now runs through the Checkout framework like any other purchase, so there is one path that takes money and one ledger that records it. The session-based signup flow, its handlers, endpoints, admin list and subscriber dashboard are removed, along with the reCaptcha and Tenant Onboarding sub-features and PaymentMethodOptions. The checkout had no subscription entry point, so this adds one: a plan contributes its recurring price and setup fee as billing items, the account step creates and signs in a buyer who is not logged in, and the content step collects whatever the plan asks for. Subscription reports are rebased onto the payment ledger and the durable agreement, and reported per currency rather than summed into a site-wide total. Trials were plumbed to Stripe but unreachable, so the plan editor gains a Free Trial Days field. Standing the suite up and buying something found eight defects that the unit tests could not, because in each case the page returned 200 and quietly did the wrong thing: - A concealed step's charges vanished from the invoice, so a subscription checkout totalled 0.00 and completed for nothing. - Subscriptions, coupons and provisioning jobs were never declared in StoreCollectionOptions, so their document tables were never created. - Five editors rendered empty because a sealed view model cannot be proxied, including the payment method list. - The checkout scripts never loaded: OrchardCore.ResourceManagement is not transitive through DisplayManagement, so the tags were literal. - Two admin screens threw because a partial sat outside Views/Shared. - A promotion code was silently dropped and the customer charged full price, and the invoice was never rebuilt to apply it. - The handler hooks that stop payment being reached early were declared but never raised. - The invoice was built in the site currency, not the item's. Each has a regression test; two read the source tree so a missing collection or package reference fails the build, not a purchase. Verified in a browser end to end: plan creation, the public plan list, checkout, Pay Later payment, the durable agreement, transaction and payment ledgers, coupon apply and redemption, the reports, an anonymous purchase that creates and signs in an account, and a plan's role entitlement reaching the buyer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading the finished implementation against the plan, rather than against its intent, found defects the browser run could not show, because each appears only over time, at a gateway, or under failure. Money: - A free trial could never complete: the gateway reported zero collected and reconciliation refused it as a short payment. A deferred first cycle now expects nothing; the gateway is still told the real price and the deferral. - First-cycle coupons were display-only, and the Stripe recurring price was derived from the first attempt, so a discount would have recurred forever. The recurring price now comes from the plan's cycle amount; the first-cycle reduction is a single-use Stripe coupon and a smaller first Pay Later debt. - Billing-cycle limits were ignored at Stripe for inline prices, so a plan sold as three cycles billed until cancelled. Inline agreements carry cancel_at for the end of the last cycle. Lifecycle: - Gateway renewals never advanced the local agreement; a subscription cycle payment now records the renewal idempotently against the period the gateway named. - Offline agreements were never renewed; the lifecycle sweep advances any agreement whose provider has no gateway, and converts an ended offline trial into its first billed cycle. - Webhook and sweep completion was blocked for signed-in buyers because the account step read the request's user, which a webhook lacks. It now reads the checkout's owner. Engine: - A fulfilment failure after payment lost its own status write (YesSql discards everything written after a cancel) and was never retried. Confirmed payments are committed before fulfilment; a fulfilment that throws leaves the checkout for the sweep instead of marking it failed. - CheckoutSession had no YesSql identity, so saving it after a commit inserted a second document; a completed checkout existed twice. - Abandoned checkouts never expired. ICheckoutEngine.ExpireAsync and a session lifetime setting release what a provider was holding, never a checkout that collected money. - A begin that failed part-way committed after its lock; a provisioning claim was not committed before the slow work; the status endpoint did not check ownership; a checkout submitted before any payment was begun was moved to a state its own page refused to show. Each defect has a regression test, including a SQLite-backed store test for the identity. Verified by an HTTP-driven purchase against a fresh site: agreement created, debt recorded, role granted, one document. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exercised the checkout, subscription and refund paths against a real Stripe test account, using Stripe test clocks to advance through real billing cycles. That confirmed trials, fixed-cycle limits, first-cycle coupons, gateway renewals, dunning and refunds end to end, and surfaced four defects that a fake gateway cannot show. A plan with a setup fee could not be paid by card at all. The browser tokenizes one card for the whole checkout and confirms both obligations with it, but the one-time payment intent was created with no customer while the agreement had attached that card to one, so Stripe refused the confirmation. The two obligations now resolve a single customer keyed by the checkout, and the client's provider data reaches the one-time path as well as the recurring one. Cancelling never reached the gateway: CancelRecurringAsync was implemented by both providers and called by nothing, so a subscriber who cancelled kept being charged. Suspending had the same shape, and worse, claimed "billing was suspended" while collection continued. A lifecycle handler now pushes both to the provider, with pause expressed at Stripe through pause_collection. A gateway notification could also quietly undo a local suspension, because a gateway that suspends collection still reports the agreement as active. A local suspension, like a local cancellation, is now treated as a decision the gateway cannot express, and synchronizations name their source so a change the gateway reported is never sent back to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sold an Orchard Core site end to end on a live installation: a plan carrying the tenant-onboarding part, a buyer naming their site and administrator during checkout, a real Stripe card, and the provisioning sweep building the tenant. Nothing gave the new tenant a database, so setup refused every attempt with "DatabaseProvider setting is required" and no purchased site could ever be created. The database new sites are created on is now configured once under tenant onboarding and defaults to Sqlite, so selling a site works before anything is configured; sites sharing a database each get a table prefix derived from their name. A job that failed once could then never succeed. Setup registers the tenant before it creates anything, so a failed attempt left the name behind in the running process and every retry failed with a name collision the previous attempt had invented. An uninitialized shell left by a failed attempt is no longer read as a name that is taken. A setup failure reported as a message rather than an exception also left nothing in the log, and the job keeps only the newest message, so the first attempt's reason — usually the real one — was lost. Every failed attempt is now logged with its own reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan task 1.1. The durable payment attempt and refund ledger lived in Checkout while the module that reports on it, settles from it, and administers it is Transactions — so the types now live where they are owned, and the reference points one way: Checkout writes to the ledger, the ledger knows nothing of the checkout. PaymentAttempt, PaymentAttemptState, PaymentRefund, RefundStatus and the two store contracts move to Transactions.Abstractions; the stores and their index providers move to Transactions.Core. The financial-document contracts move there too — a receipt or invoice number belongs with the ledger that issues it — which empties Commerce.Abstractions, so that project is deleted. The ledger's index tables deliberately stay in CheckoutMigrations rather than splitting to TransactionMigrations as the plan sketched: Checkout registers the index providers, so splitting them would either create tables nothing writes to or register index providers for tables that were never created. A new architecture test asserts the direction at the project-reference level: no Transactions project may reference anything under Checkout except its abstractions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan tasks 5.1 to 5.4. A product carried one amount, so selling the same thing monthly and annually, at a seat price, or for whatever the buyer thinks it is worth meant a separate product for each. ProductPricePart adds repeatable prices to any product: one-time or recurring, each with its own interval, cycle limit, trial, setup fee, optional buyer-named amount within bounds, optional quantity, and an availability window. A product with no prices is still sold at the single amount on its product part, so the part is opt-in and nothing has to be migrated. The price resolver became the place the rules live. It picks the price the buyer chose or the default, refuses one that is not on offer, refuses an amount outside the bounds or a quantity beyond the maximum, and never converts currency. Those are enforced server-side because every one of them arrives from a form: a checkout may be started by anyone who can craft the request. The plan card turns into a chooser when there is a choice to make and stays a single button when there is not. The chosen price rides on the checkout session rather than the request, so it survives the buyer leaving the page and is still there when a provider notification completes the purchase with no browser involved. At Stripe a fixed price now becomes one reusable Price identified by a lookup key that includes the amount, so raising a price creates a new one instead of silently repricing everyone already subscribed; an amount the buyer named has no reusable offer behind it and stays inline. The catalog keeps its own billing-interval vocabulary rather than borrowing the payments one, so Products still does not depend on the purchasing pipeline — the architecture test that says so caught the attempt. The checkout handler is where the two meet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buying a plan on its annual price charged the monthly one. Starting a checkout builds its steps and their charges from the session, and the chosen price was put on the session after that had already happened — so the handler that priced the plan never saw a choice and fell back to the default. The invoice said "monthly", the checkout completed, and the buyer simply got the wrong thing at the wrong price. Only buying each price against a real gateway showed it. The session is now seeded before the flow is built, which is the rule that matters: anything deciding what is being bought has to be there when the steps and their charges are created. A regression test pins the ordering. The price editor also always offers one blank row. Without it a product with no prices had no row to type into and nothing for the browser to copy, so the first price could never be added. Docs updated for the new prices, the checkout selection, the ledger's move to Transactions, and a stale line claiming entitlement revocation was missing — it is not; the handler that grants a plan's roles revokes them when the agreement stops being current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running it needed no clock to be faked. The sweeps already take IClock; what they need is an agreement whose next billing date has passed, which is the state a month of elapsed time produces. Seeding that and letting the real sweeps run showed both halves: the lifecycle sweep advanced the agreement, and the Pay Later sweep invoiced the next cycle and noted it on the previous one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answering "does everything have unit tests" honestly: four pieces of the pricing work did not. The plan handler had none at all, which is the worst place for a gap — it is where a plan becomes money, and the wrong branch there is silent. Thirteen tests now pin the amount, the interval mapping in both directions, quantity as a subtotal, the setup fee as its own line, trials and cycle limits, which prices name a reusable offer, the fall back to the plan part, and the refusal to sell a one-time price as a subscription. Reverting the year mapping fails two of them. The Stripe lookup key had none either, though the plan asked for them: the key changes with the amount and with the interval, and is absent for a buyer-named amount or a group assembled from several prices. The editor's validation rules moved out of the display driver into ProductPriceEditor, because rules buried in a driver cannot be tested without building a shape harness this suite does not have — and they were worth testing. The driver is now what a driver should be: it binds a form and renders the messages those rules produce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #11
This pull request introduces several foundational changes to support a reusable e-commerce architecture, along with various project and dependency updates. The most significant addition is a comprehensive decision record documenting binding architectural choices for the e-commerce foundation, ensuring consistency and future-proofing before domain-specific modules are developed. Additionally, the solution and package configuration files have been updated to include new e-commerce-related modules and dependencies.
Solution and module structure updates:
CrestApps.OrchardCore.slnxto include new abstraction, core, and module projects for e-commerce domains such as Addresses, Checkout, Payments, Receipts, Subscriptions, Taxation, Transactions, Products, Commerce, PayLater, Stripe, and others, ensuring these modules are part of the solution and available for development and testing. [1] [2] [3]Dependency and build configuration:
Directory.Packages.props, includingStripe.net,OrchardCore.ContentManagement.Display,OrchardCore.ReCaptcha.Core,OrchardCore.RateLimits.Core,OrchardCore.Setup.Abstractions, andOrchardCore.Users, to support new commerce and security features. [1] [2] [3] [4] [5]Directory.Build.propsto allow inheriting parent props files, enabling easier override of build properties such as target frameworks in multi-repo or nested scenarios.