Repository navigation
Releases: contenir/contenir-commerce
Releases · contenir/contenir-commerce
Release list
v2.0.0-RC2
Follow-ups to RC1. See "From 2.0.0-RC1 to 2.0.0-RC2" in UPGRADE-2.0.md.
Added
Order\CheckoutService(createPendingOrder(),beginCheckout(),purchaseItemsFor()),
Order\CompletionService(completeFromCheckoutSession(),expireCheckout()) andOrder\FulfilmentService
(cancelOrder(),markAwaitingPickup(),markCollected(),refundOrder()), each with a factory and registered in
the container. Inject one directly when a class needs only its part of the lifecycle.PaymentGatewayInterface::expireCheckoutSession(), implemented byStripeGateway(Stripe's
checkout.sessions.expire) andUnconfiguredGateway.CheckoutSession::isOpen()and theSTATUS_OPENand
STATUS_EXPIREDconstants.ArtworkRepository::claim(): marks a work sold only if it is still available, in one conditionalUPDATE.Config\CommerceSettingsandConfig\Factory\CommerceSettingsFactory, from the newcontenir_commerceconfig
key:order_reference_prefix(defaultLR),currency(AUD),tax_rate(10) andtax_label(GST).Money\TaxRate, a rate in integer parts per million;Money::gstComponent()takes an optionalTaxRate.Abstract*Entitybase classes for the six entities, with the shipped entities as their final defaults, and the
contenir_commercekeysartwork_entity,order_entity,order_item_entity,artist_enquiry_entity,
artist_enquiry_file_entityandemail_log_entitynaming the class each repository hydrates.
AbstractArtworkEntity::getTitle()is a hook for sites that map a title column. Every repository gains
newEntity().Model\Repository\Factory\RepositoryFactory, which builds the repositories with the configured entity classes.Exception\PurchaseItemMismatchException, andConfigurationException::invalidEntityClass(),invalidSetting()
andunknownRepository().
Changed
- Custom payment gateways:
PaymentGatewayInterfacegainsexpireCheckoutSession(string $sessionId): CheckoutSession. Implementations outside this package must add it (see UPGRADE-2.0.md). OrderManageris a façade over the three services, with the same public methods. Its constructor takes
(CheckoutService, CompletionService, FulfilmentService). The container builds it unchanged.cancelOrder()expires the Stripe checkout session of a pending order whose checkout has begun, before cancelling
it. A session that is already complete or expired is left as it is, and the order is still cancelled; a session
paid before the cancellation is refunded when it completes (RefundedCancelled), as in RC1. When Stripe cannot be
reached,PaymentFailedExceptionpropagates and the order stays pending.createPendingOrder()checks each item's price, and its title when the artwork entity maps one, against the
artwork as stored, and throwsPurchaseItemMismatchExceptionon a difference.- The order reference prefix, Stripe currency, tax rate and tax label come from
CommerceSettings. The defaults
reproduce RC1. - Repositories take the entity class to hydrate as an optional last constructor argument, and return
Abstract*Entitytypes.ArtworkRepositoryalso takes the database adapter and contenir-db-model's
TypeRegistry:(EntityManager, AdapterInterface, TypeRegistry, string $entityClass = ArtworkEntity::class).
CompletionResult::$orderand theOrderManagermethods are typed againstAbstractOrderEntity. - The repositories are built by
Model\Repository\Factory\RepositoryFactoryinstead of contenir-db-model's
Container\RepositoryFactory. StripeGatewaytakes the currency as an optional third constructor argument (AUD);StripeGatewayFactory
passes the configured one.OrderManager'stoo-many-methods,cyclomatic-complexity,kan-defectandexcessive-parameter-listMago
expectations are gone.
Fixed
- Two payments for the same work completing at the same instant could both pass the availability check and both
sell it. Completion now claims each work withUPDATE artwork SET status = 'sold' ... WHERE artwork_id = ? AND status = 'available'inside the completion transaction and checks the affected rows, on SQLite, MySQL and
PostgreSQL alike. When a claim fails the claims already made are rolled back and the payment is refunded in full
(RefundedRace, idempotent). - When the webhook and the thank-you page completed the same order at once, the second could find the work already
claimed (by its own order) and refund the winning payment. The race-refund path now re-reads the order and reports
AlreadyCompletedwhen it has already been settled. createPendingOrder()trusted the caller's prices. A cart built from stale or tampered data is now refused.- A pending order cancelled by staff left its checkout session open, so the buyer could still pay for it until the
session expired (the payment was then refunded). The session is now expired on cancellation.
v2.0.0-RC1
The port to contenir-db-model 2, under the new package name. See UPGRADE-2.0.md.
Changed
- Renamed from
contenir/commercetocontenir/contenir-commerce. The package declaresreplacefor the old name;
requirecontenir/contenir-commerceinstead. The namespace staysContenir\Commerce. - Requires PHP 8.3, 8.4 or 8.5, contenir/contenir-db-model 2 (php-db/phpdb 0.6) and stripe/stripe-php 22 (was 17;
the pinned Stripe API version moves from abasilversion,2025-03-31to2025-08-27, to2026-09-30.endive). - Entities are attribute-mapped final classes with typed camelCase properties; statuses are enums, timestamps
DateTimeImmutable, to-many relationsCollections. The tables and columns are unchanged: no migration. - Repositories are final, extend
Contenir\Db\Model\Repositoryand take theEntityManager.create(),
findOne(),save()andgetTable()are gone: build entities withnew, find withfind()/findBy()/
findOneBy()or the typed finders, and save withEntityManager::save(). OrderManagertakes theEntityManageras its first argument.createPendingOrder()and completion each write in
one transaction.ConfigProvider::__invoke()returnsdependencies(wasservice_manager);Module::getConfig()returns the same
services underservice_manager.getDependencyConfig()is nowgetDependencies(). Entities are no longer
registered as services; repositories use contenir-db-model'sContainer\RepositoryFactory.PaymentGatewayInterface::refund()takes an optional$idempotencyKey, sent to Stripe asIdempotency-Key.CheckoutSessioncarriespaymentStatus;isPaid()is true only when the session is complete and paid.beginCheckout()re-checks availability and refuses an order that is not pending or has already begun checkout.CheckoutRequestrejects an expiry over 1,440 minutes (Stripe's maximum) as well as under 30.Moneyarithmetic is integer only: GST is computed withintdiv()andformat()never goes through a float.
Sums and products beyond the integer range throwOverflowException.StripeGatewayFactoryreads the clock from the container'sClockInterfaceinstead of constructing its own, and
rejects a mistypedstripeconfig withConfigurationException.- Value objects (
Money,CheckoutRequest,CheckoutSession,PurchaseItem, ...) arereadonlyclasses. Every
exception implementsException\ExceptionInterface; invalid arguments throw the package's
InvalidArgumentException(a subclass of the SPL one), an unknown checkout sessionOrderNotFoundExceptionand a
refund without a paymentPaymentFailedException(both stillRuntimeExceptions). - Mago replaces phpcs and PHPStan; PHPUnit unit and integration suites, Infection (MSI 100%) and Codecov in CI.
Added
OrderRepository::findOneByCheckoutSessionId(),findOneByOrderRef()andfindByStatus();
OrderItemRepository::findByOrderId();ArtistEnquiryRepository::findByStatus();
ArtistEnquiryFileRepository::findByArtistEnquiryId();EmailLogRepository::findByOrderId()and
findByArtistEnquiryId();ArtworkRepository::findCurrent().ArtworkEntity::isAvailable()andgetPrice(),OrderEntity::getId(),getTotal()andgetGstAmount(),
OrderItemEntity::getPrice(),Money::zero().CompletionOutcome::RefundedCancelled.Exception\ExceptionInterface,InvalidArgumentException,OverflowException,OrderNotFoundExceptionand
ConfigurationException.
Fixes
- A checkout session that was complete but not yet paid was fulfilled. With a delayed payment method (BECS Direct
Debit, for example) the order was marked paid and the works sold before the funds arrived. Completion now requires
Stripe'spayment_statusto bepaidand reportsNotPaiduntil then; call it again on
checkout.session.async_payment_succeeded. - Beginning checkout twice for one order overwrote the first session's id, so a payment through the first session
matched no order: the money was taken and the order never completed. Checkout could also begin for an order that
was already paid or cancelled. Both are now refused withInvalidTransitionException. - A payment for an order cancelled while the buyer was paying was kept and reported as
AlreadyCompleted. It is now
refunded in full (RefundedCancelled) and the order stays cancelled. - Completion saved the paid order and then each sold work separately, so a failure part-way left a paid order whose
works were still for sale. It is now one transaction.createPendingOrder()likewise could leave an order without
its lines. - A webhook retry after a race refund succeeded but before the order was saved refunded again; Stripe refused the
second refund and the order stayed pending. Race refunds now carry an idempotency key and run inside the
completion transaction. - A race with no payment intent on the session marked the order refunded without refunding anything. It now throws
PaymentFailedException. - stripe-php errors other than
ApiErrorException(itsInvalidArgumentExceptionfor a blank session id, for
example) escapedStripeGatewayunwrapped. Every stripe-php exception now becomesPaymentFailedException. - The same work could appear twice in one order and be charged twice.
createPendingOrder()now rejects it. ArtworkUnavailableException::getTitles()threw anErroron an exception built withnew. The constructor is
now private; useforTitles().
Removed
- The magic-property entities of contenir-db-model 1 (
getArrayCopy(), array constructors,$columns). - Entity registrations in the container.
- phpcs, PHPStan and their configuration.