← Back to blog

May 15, 2026 · 10 min read

How RestoPick Pro Grew One Feature at a Time

Every feature in Pro came out of two kitchens I maintain, and it has never been sold to anyone. This is where menu hours, recurring closures, tips, kitchen ticket printing, and the Square Connection came from.

In production at Ramen Nagomi and Padi Los Angeles.

The free version of RestoPick handles the everyday case. Pickup date, pickup time, weekly schedule, closed dates. If a restaurant has one menu and stable hours, it’s enough.

Pro is what happened when the two kitchens I maintain kept asking for more.

Every feature in Pro exists because one of two restaurant owners sent me a message that started with “is it possible to…” or “we keep having to manually…”. I’d look at what they were doing, look at where the free plugin stopped, and decide whether the gap was big enough to be its own feature. Sometimes it was. This post walks through the ones that made the cut.

Worth saying up front: Pro isn’t for sale. It’s a private layer running on the two sites I maintain. I’m writing it up because the problems were interesting, not because there’s a checkout button at the end.

Both owners asked for this one, and they asked first.

Some menus are time-bound: a breakfast menu only available 6–10 am, weekend brunch only on Saturday and Sunday, dinner specials only after 5 pm. The free plugin’s hours apply to the whole restaurant, so a customer can drop pancakes in their cart at 8 pm and the checkout will happily accept a 9 pm pickup. That bug surfaced about a week into the first restaurant going live.

In Pro, every product or category can declare its own pickup window. The checkout time dropdown narrows live based on what’s in the cart, and if multiple products with different windows are in the same cart, the dropdown shows the intersection.

The bit I like: when the cart contains nothing time-bound, the narrowing script never even loads. Customers whose order isn’t affected pay nothing for the feature.

Recurring Closed Days

The free plugin stores closed dates as a flat list. That works for “Christmas Day” and “the day the renovation finishes”. It doesn’t work for “every Monday for the next two years”, because nobody wants to hand-type 104 Mondays.

Pro adds three rule types (weekly, monthly nth-weekday like “every first Sunday”, and yearly) and expands them on demand into the same list the free plugin already uses:

add_filter( 'restopick_pickup_closed_dates', array( $this, 'inject_dates' ), 10, 3 );

Rules expand only within the window the calendar is actually asking about, so runtime cost stays proportional to how far forward the checkout looks, not to how many rules the owner configured. The free side doesn’t know or care that the dates came from a rule.

Tips at checkout

One of the two kitchens runs on tips for its servers and runners, and wanted a tipping flow that wasn’t another plugin to install and configure. So there’s a Tip section with configurable preset percentages plus a custom amount.

The tip is written as a WooCommerce fee, which means reports, emails, and any payment gateway that already understands WC fees handle it correctly without me writing a parallel system. When orders are paid through the companion Square gateway, tips are recognised by fee name (“Tip” or “Gratuity”) and routed through Square’s Payment.tip_money so they land in the right place for accounting.

The shortcode

The free plugin renders pickup fields inside the WooCommerce checkout. Most of the time that’s exactly where they belong.

One owner wanted the pickup time picked before the customer commits to a cart, on a landing page rather than buried in checkout. Pre-committing to a slot tends to lower checkout abandonment (you’ve already invested in the choice) and lets the menu reflect what’s available for that slot.

Pro adds [restopick_form], which renders the same fields with the same validation, sharing session storage with the WC checkout. A date picked in a popup carries through to checkout without re-entry.

Auto-print kitchen tickets

This one removed a chore somebody was doing every single day.

Some kitchens work off printed dockets and nothing else. One of mine does. When a pickup order came in through WooCommerce, someone in the office had to open the admin, find the order, click the printer icon, and walk the docket to the kitchen. Sometimes orders were missed. Sometimes the same order got printed twice because two people both saw it.

Pro can send a plain-text ticket to a thermal printer the moment an order transitions to processing or completed, either through PrintNode (the cloud-print service that works with most ESC/POS printers) or a generic webhook to whatever bridge the restaurant has.

The runtime is built around two things I cared about:

  1. It must not print twice. A status transition from processing to completed looks like two events to WordPress; both shouldn’t print. An order-meta flag pinned the moment one successful print happens is the entire mechanism.
  2. It must retry without burying failures. A jammed printer or a flaky network shouldn’t silently swallow a ticket. Exponential backoff retries run via WP cron (1 minute, 5 minutes, 15 minutes), with an admin email when retries are exhausted and a per-order log in the admin. Every attempt is visible.

For restaurants whose workflow is “tickets come out of the kitchen printer, period”, nobody has to stand between WooCommerce and the kitchen any more.

Square Connection

Pro hosts a Square Connection sub-page that owns one thing: your Square credentials. Application ID, Access Token, and Location ID, in both sandbox and production sets, with a mode toggle on top and a Test Connection button that pings /v2/locations and confirms the configured location’s currency matches WooCommerce. It’s a 30-second sanity check before you push real orders.

This page exists because Pro doesn’t actually take payments. The companion RestoPick Square Payment gateway does, and it needs credentials at checkout time. Centralising them in Pro means the gateway plugin doesn’t ask for the same Application ID, Access Token, and Location ID a second time. One place to rotate keys, one place to switch between sandbox and production.

Switching mode from sandbox to production (or back) triggers a confirmation dialog before save. The consequence is that future orders use the new mode’s credentials, and that’s not the kind of thing you want to fat-finger.

The mode also gets stamped onto every order’s meta at payment time. So a refund issued six months later resolves to the right Square account by reading that stamp, not whatever mode the admin currently has selected. An order placed in sandbox stays refundable against sandbox credentials even after the admin has flipped to production.

One thing Pro used to do and doesn’t anymore, and this is the part of the whole project I’d most like back.

The feature I shipped and then deleted

Pro 1.4.0 added a Square POS Sync sub-page. WooCommerce orders paid through non-Square gateways (Stripe, PayPal, COD) got pushed into Square POS after checkout as a post-payment dispatch: order body built from the WC order, PICKUP fulfillment carrying the scheduled time converted to UTC, taxes back-derived from WC_Order::get_total_tax(), WC fees mapped to service charges, retry cron with backoff, admin failure email, per-attempt order notes. It is the largest single entry in Pro’s changelog.

Pro 1.5.0 deleted it.

The problem is that I was building the Square payment gateway at the same time, and the two features create the same object. Run both on one store and a single sale produces two Square-side records: one Square Order created by the gateway at checkout, and a second created by the POS sync after payment. Two records for one sale, in the system the restaurant reconciles its books against.

Neither plugin noticed. The sync was written first, under the entirely reasonable assumption that nothing else was creating Square Orders for these sales. The gateway made that assumption false and there was no code path where anything checked. Both features worked exactly as designed, which is why it took looking at a Square dashboard rather than a log to see it at all.

The fix was to pick one moment of creation and commit to it. The gateway builds the Square Order at checkout, reusing the sync’s builder rather than growing a second one, and stamps the WC order as synced so the sync would skip it if it ever came back. Exactly one Square Order per sale.

What’s left is stranger than a clean delete. The sync’s orchestrator (status hooks, retry cron, failure email) is still sitting in the tree and is never instantiated. Its static builder methods are very much alive, because that’s precisely what the gateway calls to shape the order. So one file is half dead code and half the most load-bearing code in the family, and telling the halves apart by reading it is not obvious. I wrote the reasoning into an architecture decision record in the repo, with a note not to re-instantiate the orchestrator without re-opening the decision first, mostly addressed to whoever I am in six months.

The honest summary is that I built a whole feature, discovered it double-counted money against another feature I was building, and removed it one version later. It cost me the largest thing I’d written for Pro. It is also the only reason the architecture is now something I can explain in a paragraph.

REST API

A tiny endpoint to flip pickup on or off remotely, authenticated with a Bearer token:

POST /wp-json/restopick/v1/pickup/disable
Authorization: Bearer <token>

Useful in two scenarios: a POS or kiosk integration needs to disable online pickup the moment the kitchen hits capacity, or staff need to pause pickup from the floor without logging into WordPress. Every request is logged with timestamp and originating IP, surfaced in the admin sub-page next to the token-management UI.

License management

I built the usual scaffolding. Enter a key, the plugin validates, rechecks every 24 hours. The recheck is async on purpose so a slow license server can never block an admin or front-end request. An inactive license disables the Pro features but leaves the free plugin running normally, so the site falls back instead of breaking.

None of it has ever run in anger. Pro isn’t sold, so there are no keys, no customers, and no license server under load. I built it because I assumed I’d sell Pro eventually. It’s been sitting there unused ever since.

What I noticed writing it

Most of Pro is the boring obvious thing done carefully: idempotent operations, async retries, and enough filters and hooks that the free side stays clean. Every Pro feature is a yes/no toggle, and the ones you don’t enable stay completely out of the way.

Two kitchens is a small sample, and I’d be wary of anyone claiming general lessons from it. It’s also the only reason any of this is specific rather than theoretical: I could watch what broke, on site, the week it broke.