Skip to content

Commit 0407ab5

Browse files
committed
Merge branch 'release/0.8.76'
2 parents 1fa4f7c + 83336e6 commit 0407ab5

65 files changed

Lines changed: 7970 additions & 6 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 8,
5-
"patch": 75,
5+
"patch": 76,
66
"build": 0
77
}

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"neuron-php/jobs": "0.2.*",
2020
"neuron-php/orm": "0.1.*",
2121
"neuron-php/dto": "0.0.*",
22+
"neuron-php/payments": "0.3.*",
2223
"rlanvin/php-rrule": "^2.6",
2324
"phpmailer/phpmailer": "^6.9",
2425
"cloudinary/cloudinary_php": "^2.0",
@@ -37,6 +38,9 @@
3738
"codeception/module-asserts": "^3.2",
3839
"codeception/module-db": "^3.2"
3940
},
41+
"suggest": {
42+
"neuron-php/payments": "Adds Stripe-backed donation/payment processing and enables the [donation] shortcode."
43+
},
4044
"autoload": {
4145
"psr-4": {
4246
"Neuron\\": "src/"

resources/.cms-manifest.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
"20260618120000_add_external_url_to_events.php",
2525
"20260619140000_add_recurrence_to_events.php",
2626
"20260619140001_create_event_recurrence_exceptions_table.php",
27-
"20260619140002_add_occurrence_date_to_event_registrations.php"
27+
"20260619140002_add_occurrence_date_to_event_registrations.php",
28+
"20260622000000_create_payments_table.php",
29+
"20260622000001_create_subscriptions_table.php",
30+
"20260623000000_create_products_table.php",
31+
"20260623000001_create_order_items_table.php"
2832
],
2933
"config_files": [
3034
"auth.yaml",
@@ -42,14 +46,17 @@
4246
"calendar",
4347
"contact",
4448
"content",
49+
"donations",
4550
"email",
4651
"emails",
4752
"home",
4853
"http_codes",
4954
"layouts",
5055
"member",
5156
"pages",
52-
"partials"
57+
"partials",
58+
"payments",
59+
"store"
5360
],
5461
"public_assets": [
5562
".htaccess",

resources/config/neuron.yaml.example

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,95 @@ contact:
248248
- { name: name, label: "Your Name", type: text, required: true, sender_name: true, rules: { length: { min: 2, max: 80 } } }
249249
- { name: email, label: "Email", type: email, required: true, reply_to: true }
250250
- { name: message, label: "Message", type: textarea, required: true, rules: { length: { min: 10, max: 4000 } } }
251+
252+
# Payments & Subscriptions Configuration
253+
# Required only by sites that take payments (donations, memberships, ...).
254+
# Install the optional package first: composer require neuron-php/payments
255+
#
256+
# A single engine drives every payment form via the [payment form="key"]
257+
# shortcode ( [donation] is a friendly alias ). One-time payments and recurring
258+
# subscriptions are both supported; each renewal is recorded and the
259+
# subscription lifecycle ( renewals, cancellations, failed payments ) is driven
260+
# by a signed webhook at /payments/webhook.
261+
#
262+
# Keep secret_key and webhook_secret in your encrypted secrets file
263+
# (secrets.yml.enc), NOT in this committed config. Only provider/currency and
264+
# the non-secret form definitions are safe to keep here.
265+
#
266+
# Gateway keys:
267+
# provider stripe (only supported provider today)
268+
# secret_key Stripe secret API key (sk_live_... / sk_test_...)
269+
# publishable_key Stripe publishable key (pk_...), if you need it client-side
270+
# webhook_secret Stripe signing secret for /payments/webhook (whsec_...)
271+
# currency ISO 4217 currency code (default: usd)
272+
#
273+
# Form keys (under payments.forms):
274+
# purpose what the payment is for: donation (default), membership, ...
275+
# label heading shown above the form
276+
# button submit button label
277+
# to internal email notified when a payment completes
278+
# product_name line-item label shown on the Stripe Checkout page
279+
# success_message thank-you message on the success page
280+
# amounts preset amount tiers in major units (e.g. dollars)
281+
# allow_custom_amount true|false - show a "custom amount" input
282+
# min_amount minimum accepted amount in major units
283+
# frequencies allowed cadences: one_time, monthly, quarterly,
284+
# semiannual, annual (omit/[] for one-time only)
285+
# fields payer fields (same schema as contact form fields)
286+
#
287+
# Backward compatibility: a legacy top-level `donations:` section (with the same
288+
# default_form/success_url/cancel_url/forms keys) is still read if `payments`
289+
# has no forms, so existing donation configs keep working.
290+
payments:
291+
provider: stripe
292+
secret_key: ""
293+
publishable_key: ""
294+
webhook_secret: ""
295+
currency: usd
296+
default_form: general # Form rendered by a bare [payment]/[donation] shortcode
297+
success_url: "/payments/success"
298+
cancel_url: "/payments/cancel"
299+
forms:
300+
general:
301+
purpose: donation
302+
label: "Make a Donation"
303+
button: "Donate"
304+
to: "info@yourdomain.com"
305+
product_name: "Donation"
306+
success_message: "Thank you for your generous donation!"
307+
amounts: [ 25, 50, 100, 250 ]
308+
allow_custom_amount: true
309+
min_amount: 5
310+
frequencies: [ one_time, monthly, quarterly, semiannual, annual ]
311+
fields:
312+
- { name: name, label: "Your Name", type: text, required: true, sender_name: true, rules: { length: { min: 2, max: 80 } } }
313+
- { name: email, label: "Email", type: email, required: true, reply_to: true }
314+
- { name: phone, label: "Phone", type: tel, required: false }
315+
316+
# ---------------------------------------------------------------------------
317+
# Store / products ( optional )
318+
#
319+
# The catalog itself is managed in the admin ( Content > Products ), not here.
320+
# A cart purchase is recorded as a payment with purpose "order" and reuses the
321+
# same Stripe gateway, webhook ( /payments/webhook ), and admin as donations.
322+
# This section only configures the storefront return URLs and notifications.
323+
#
324+
# title heading for a bare [products] grid
325+
# currency ISO 4217 ( defaults to payments.currency )
326+
# success_url return URL after a completed order
327+
# cancel_url return URL when the buyer cancels
328+
# notification_email internal recipient for new-order notifications
329+
# notification_subject / receipt_subject optional email subject overrides
330+
#
331+
# Shortcodes:
332+
# [products] grid of active products with add-to-cart
333+
# [products limit="6"] capped grid
334+
# [product id="3"] a single product card
335+
# [product slug="t-shirt"] a single product card by slug
336+
# [cart] a cart link / summary button
337+
store:
338+
title: "Shop"
339+
currency: usd
340+
success_url: "/store/success"
341+
cancel_url: "/store/cancel"
342+
notification_email: "orders@yourdomain.com"

resources/config/services.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,23 @@ services:
8383
Neuron\Cms\Repositories\DatabaseContactSubmissionRepository:
8484
type: autowire
8585

86+
Neuron\Cms\Repositories\DatabasePaymentRepository:
87+
type: autowire
88+
89+
Neuron\Cms\Repositories\DatabaseSubscriptionRepository:
90+
type: autowire
91+
92+
Neuron\Cms\Repositories\DatabaseProductRepository:
93+
type: autowire
94+
95+
Neuron\Cms\Repositories\DatabaseOrderItemRepository:
96+
type: autowire
97+
98+
# Builds the optional payment gateway from the `payments` config section.
99+
# Safe to autowire whether or not neuron-php/payments is installed.
100+
Neuron\Cms\Services\Payment\PaymentGatewayFactory:
101+
type: autowire
102+
86103

87104
# ============================================================================
88105
# REPOSITORIES - Interface Bindings
@@ -136,6 +153,22 @@ services:
136153
type: alias
137154
target: Neuron\Cms\Repositories\DatabaseContactSubmissionRepository
138155

156+
Neuron\Cms\Repositories\IPaymentRepository:
157+
type: alias
158+
target: Neuron\Cms\Repositories\DatabasePaymentRepository
159+
160+
Neuron\Cms\Repositories\ISubscriptionRepository:
161+
type: alias
162+
target: Neuron\Cms\Repositories\DatabaseSubscriptionRepository
163+
164+
Neuron\Cms\Repositories\IProductRepository:
165+
type: alias
166+
target: Neuron\Cms\Repositories\DatabaseProductRepository
167+
168+
Neuron\Cms\Repositories\IOrderItemRepository:
169+
type: alias
170+
target: Neuron\Cms\Repositories\DatabaseOrderItemRepository
171+
139172

140173
# ============================================================================
141174
# AUTHENTICATION SERVICES
@@ -350,6 +383,7 @@ services:
350383
- '@Neuron\Cms\Repositories\DatabaseEventCategoryRepository'
351384
- '@Neuron\Data\Settings\SettingManager'
352385
- '@Neuron\Cms\Repositories\DatabaseEventRegistrationRepository'
386+
- '@Neuron\Cms\Repositories\DatabaseProductRepository'
353387

354388
Neuron\Cms\Services\Content\ShortcodeParser:
355389
type: create
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
use Phinx\Migration\AbstractMigration;
4+
5+
/**
6+
* Create payments table.
7+
*
8+
* Stores every individual charge initiated through the [payment] / [donation]
9+
* shortcodes: one-time payments AND each recurring subscription invoice ( the
10+
* initial charge and every renewal each get their own row ). Fixed columns
11+
* capture the gateway/transaction state ( session, payment intent, invoice,
12+
* subscription, amount, status ) while the JSON `payload` column holds the
13+
* dynamic per-form payer field values, mirroring the contact_submissions
14+
* design so the schema stays stable regardless of each form's field set.
15+
*
16+
* `purpose` ( e.g. donation, membership ) tags what the payment is for, so a
17+
* single engine serves donations, memberships, dues, etc. A payment is created
18+
* with status "pending" before the payer is redirected to the gateway, then
19+
* flipped to "completed" by the verified webhook.
20+
*/
21+
class CreatePaymentsTable extends AbstractMigration
22+
{
23+
/**
24+
* Create payments table
25+
*/
26+
public function change()
27+
{
28+
$table = $this->table( 'payments', [ 'signed' => false ] );
29+
30+
$table->addColumn( 'purpose', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'donation' ] )
31+
->addColumn( 'form_key', 'string', [ 'limit' => 64, 'null' => false ] )
32+
->addColumn( 'provider', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'stripe' ] )
33+
->addColumn( 'type', 'string', [ 'limit' => 16, 'null' => false, 'default' => 'one_time' ] )
34+
->addColumn( 'session_id', 'string', [ 'limit' => 255, 'null' => true ] )
35+
->addColumn( 'payment_intent_id', 'string', [ 'limit' => 255, 'null' => true ] )
36+
->addColumn( 'invoice_id', 'string', [ 'limit' => 255, 'null' => true ] )
37+
->addColumn( 'subscription_id', 'string', [ 'limit' => 255, 'null' => true ] )
38+
->addColumn( 'amount_cents', 'integer', [ 'null' => false, 'default' => 0 ] )
39+
->addColumn( 'currency', 'string', [ 'limit' => 8, 'null' => false, 'default' => 'usd' ] )
40+
->addColumn( 'frequency', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'one_time' ] )
41+
->addColumn( 'status', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'pending' ] )
42+
->addColumn( 'payer_name', 'string', [ 'limit' => 255, 'null' => true ] )
43+
->addColumn( 'payer_email', 'string', [ 'limit' => 255, 'null' => true ] )
44+
->addColumn( 'payload', 'text', [ 'null' => false, 'default' => '{}' ] )
45+
->addColumn( 'ip_address', 'string', [ 'limit' => 45, 'null' => true ] )
46+
->addColumn( 'user_agent', 'string', [ 'limit' => 500, 'null' => true ] )
47+
->addColumn( 'completed_at', 'timestamp', [ 'null' => true ] )
48+
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
49+
->addIndex( [ 'purpose' ] )
50+
->addIndex( [ 'form_key' ] )
51+
->addIndex( [ 'status' ] )
52+
->addIndex( [ 'session_id' ] )
53+
->addIndex( [ 'subscription_id' ] )
54+
->addIndex( [ 'created_at' ] )
55+
->create();
56+
}
57+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use Phinx\Migration\AbstractMigration;
4+
5+
/**
6+
* Create subscriptions table.
7+
*
8+
* Tracks the lifecycle of a recurring agreement created through a recurring
9+
* [payment] / [donation] ( e.g. a monthly donation or a membership ). One
10+
* subscription row owns many `payments` rows ( the initial charge plus every
11+
* renewal ), linked by the gateway `subscription_id`.
12+
*
13+
* The row is created when the checkout that started the subscription completes,
14+
* its `status` / `current_period_end` are kept current by subscription and
15+
* invoice webhooks, and it is marked canceled when the agreement ends.
16+
*/
17+
class CreateSubscriptionsTable extends AbstractMigration
18+
{
19+
/**
20+
* Create subscriptions table
21+
*/
22+
public function change()
23+
{
24+
$table = $this->table( 'subscriptions', [ 'signed' => false ] );
25+
26+
$table->addColumn( 'purpose', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'donation' ] )
27+
->addColumn( 'form_key', 'string', [ 'limit' => 64, 'null' => false ] )
28+
->addColumn( 'provider', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'stripe' ] )
29+
->addColumn( 'subscription_id', 'string', [ 'limit' => 255, 'null' => false ] )
30+
->addColumn( 'status', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'active' ] )
31+
->addColumn( 'frequency', 'string', [ 'limit' => 32, 'null' => false, 'default' => 'monthly' ] )
32+
->addColumn( 'amount_cents', 'integer', [ 'null' => false, 'default' => 0 ] )
33+
->addColumn( 'currency', 'string', [ 'limit' => 8, 'null' => false, 'default' => 'usd' ] )
34+
->addColumn( 'payer_name', 'string', [ 'limit' => 255, 'null' => true ] )
35+
->addColumn( 'payer_email', 'string', [ 'limit' => 255, 'null' => true ] )
36+
->addColumn( 'payload', 'text', [ 'null' => false, 'default' => '{}' ] )
37+
->addColumn( 'current_period_end', 'timestamp', [ 'null' => true ] )
38+
->addColumn( 'canceled_at', 'timestamp', [ 'null' => true ] )
39+
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
40+
->addColumn( 'updated_at', 'timestamp', [ 'null' => true ] )
41+
->addIndex( [ 'purpose' ] )
42+
->addIndex( [ 'form_key' ] )
43+
->addIndex( [ 'status' ] )
44+
->addIndex( [ 'subscription_id' ], [ 'unique' => true ] )
45+
->create();
46+
}
47+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
use Phinx\Migration\AbstractMigration;
4+
5+
/**
6+
* Create products table.
7+
*
8+
* Backs the storefront catalog ( [product] / [products] shortcodes and the
9+
* admin Products screen ). Each product is a fixed-price, one-time item that
10+
* can be added to the cart and purchased through hosted Stripe Checkout. The
11+
* order itself is recorded in the shared `payments` table ( purpose = "order" )
12+
* with one `order_items` row per line, so the catalog stays decoupled from the
13+
* transaction record.
14+
*/
15+
class CreateProductsTable extends AbstractMigration
16+
{
17+
/**
18+
* Create products table
19+
*/
20+
public function change()
21+
{
22+
$table = $this->table( 'products', [ 'signed' => false ] );
23+
24+
$table->addColumn( 'name', 'string', [ 'limit' => 255, 'null' => false ] )
25+
->addColumn( 'slug', 'string', [ 'limit' => 255, 'null' => false ] )
26+
->addColumn( 'sku', 'string', [ 'limit' => 64, 'null' => true ] )
27+
->addColumn( 'description', 'text', [ 'null' => true ] )
28+
->addColumn( 'price_cents', 'integer', [ 'null' => false, 'default' => 0 ] )
29+
->addColumn( 'currency', 'string', [ 'limit' => 8, 'null' => false, 'default' => 'usd' ] )
30+
->addColumn( 'image_url', 'string', [ 'limit' => 512, 'null' => true ] )
31+
->addColumn( 'active', 'boolean', [ 'null' => false, 'default' => true ] )
32+
->addColumn( 'sort_order', 'integer', [ 'null' => false, 'default' => 0 ] )
33+
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
34+
->addColumn( 'updated_at', 'timestamp', [ 'null' => true ] )
35+
->addIndex( [ 'slug' ], [ 'unique' => true ] )
36+
->addIndex( [ 'active' ] )
37+
->addIndex( [ 'sort_order' ] )
38+
->create();
39+
}
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
use Phinx\Migration\AbstractMigration;
4+
5+
/**
6+
* Create order_items table.
7+
*
8+
* An order is a row in the shared `payments` table ( purpose = "order" ); this
9+
* table holds its individual lines. Each row snapshots the product name, SKU,
10+
* unit price and quantity at purchase time so the historical order is stable
11+
* even if the catalog product is later edited or removed.
12+
*/
13+
class CreateOrderItemsTable extends AbstractMigration
14+
{
15+
/**
16+
* Create order_items table
17+
*/
18+
public function change()
19+
{
20+
$table = $this->table( 'order_items', [ 'signed' => false ] );
21+
22+
$table->addColumn( 'payment_id', 'integer', [ 'null' => false ] )
23+
->addColumn( 'product_id', 'integer', [ 'null' => true ] )
24+
->addColumn( 'name', 'string', [ 'limit' => 255, 'null' => false ] )
25+
->addColumn( 'sku', 'string', [ 'limit' => 64, 'null' => true ] )
26+
->addColumn( 'unit_amount_cents', 'integer', [ 'null' => false, 'default' => 0 ] )
27+
->addColumn( 'quantity', 'integer', [ 'null' => false, 'default' => 1 ] )
28+
->addColumn( 'currency', 'string', [ 'limit' => 8, 'null' => false, 'default' => 'usd' ] )
29+
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
30+
->addIndex( [ 'payment_id' ] )
31+
->addIndex( [ 'product_id' ] )
32+
->create();
33+
}
34+
}

0 commit comments

Comments
 (0)