Building a donation block for Volto - Talk to me

We're building a donation block for Volto, and a backend addon that handles payment provider credentials. A few things we're figuring out right now, and we'd genuinely love to hear how you've handled them, or what went wrong when you tried:

  • Recurring payments: No gateway we've found gives you amount-locked or scoped tokens. So we're building a nightly job that checks what's due and charges it, instead of assuming the gateway has this feature (not all of them do). Has anyone here built recurring billing in Plone or Volto? How did it go?
  • Storing sensitive data that isn't content: We want to handle donor and subscription data the way Plone handles credentials, not in SQL, and not as full user accounts either. Has anyone solved this kind of problem without reaching for an external database?
  • Credentials in the admin UI: Masking secrets, being careful about what ever reaches a browser. What have you learned the hard way here?
  • Letting other gateways plug in later: If you've built anything with pluggable payment providers, in Plone or elsewhere, where did it get messy?

We're starting with one provider (PayFast, specifically for South African use) but we're designing it so someone else can plug in a different gateway later, with its own module, using the steps and triggers we're building now (Stripe in the US, Mollie in Europe, whatever fits where you are). Not a one-gateway block.

The code isn't public yet. We want to get it right first, and this is exactly the kind of thing this community is good at, so we'd rather ask now than after. Come tell us what you know.

2 Likes

I've built this kind of thing a few times for classic Plone (first some solutions using PloneGetPaid in the Plone 3 era, then later a custom solution which became jazkarta.shop). I'll reply to a few things here, happy to chat more at ploneconf.

Has anyone here built recurring billing in Plone or Volto? How did it go?

We want to handle donor and subscription data the way Plone handles credentials, not in SQL, and not as full user accounts either. Has anyone solved this kind of problem without reaching for an external database?

We stored order data in a custom BTree-based storage that is an attribute of the Plone site. It's indexed both by user id and date of the next payment, which makes it easy to retrieve one customer's orders for display, and to retrieve all orders that need to be processed today.

We used Stripe and stored the Stripe customer id and payment method id, which was enough to process a payment later (at least for the payment methods that support that).

We used celery to schedule the nightly job to process payments, but there'd be other ways to do that.

Credentials in the admin UI: Masking secrets, being careful about what ever reaches a browser. What have you learned the hard way here?

I haven't seen a really strong solution for this. I'd be tempted to make a new field type which:

  • stores values encrypted at rest with Fernet (symmetric encryption) — Cryptography 51.0.0-dev1 documentation using an encryption key from an environment variable. That way even if someone has a copy of the ZODB they can't read values unless they also have the key.
  • never serializes its value in the REST API
  • has a write-only widget (i.e. show asterisks to indicate that there is a value, but never send it over the wire. let users with permission save a new value)

Letting other gateways plug in later: If you've built anything with pluggable payment providers, in Plone or elsewhere, where did it get messy?

There's all kinds of mess here. I wouldn't try too hard to plan this in advance, before you're actually adding support for the next provider. You'll inevitably get something wrong and have to change it. That said, if you share what you have in mind for this interface, I can tell you whether it would make sense with Stripe or not.

Thanks so much for taking the time to reply. It is incredibly useful to hear how this played out for you, especially having built it twice across different eras of Plone - which is exactly what I was hoping to get out of this post.

We've landed in a similar place on the fundamentals (which is incredibly validating for me): flat storage as a site attribute (rather than SQL or full user accounts), and a stored gateway token (customer id + payment method, in your case) as the thing that makes a recurring charge possible without re-collecting card details. Same shape as what we've built, so that's a nice confirmation we're not missing something obvious.

One idea I'm definitely folding into what I'm busy with: indexing by next-payment-date. We're currently just re-scanning everything on read, which is fine for now but is exactly the kind of thing that gets expensive over time: the nightly job querying "what's due today," every night, forever. This is good timing to sort out properly rather than retrofit later.

Good to have what we're doing with credentials independently validated, since it's the part of this we've been most careful about getting right.

And fair point on the pluggable-provider planning! We did plan the seams in advance rather than the details, and it mostly held up when we added a second provider, though not without some real friction along the way. Sounds like that matches what you'd expect.

Will share the repos once we've got something more solid to show. Looking forward to talk more about this at PloneConf.