Customer Purchase Limits
Limits how much of a product, a variation or a category one customer may buy over a period of time, counted from real order history with refunds subtracted, shown on the product page before the cart fills up.
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/poseidonas/customer-purchase-limits/releases/download/v1.0.0/customer-purchase-limits.zipLimits how much of a product, a variation or a whole category one customer may buy over a period of time. The count comes from real order history, refunds are taken back off it, and the customer is told how many are left on the product page rather than at the checkout.
- Author: George Vasiliades - https://github.com/Poseidonas
- License: GPL-2.0-or-later
- Requires WordPress 6.7 or newer
- Requires PHP 8.0 or newer
- Requires WooCommerce 9.0 or newer, tested up to 11.0
- Compatible with High-Performance Order Storage and with the cart and checkout blocks
- Settings live in WooCommerce > Purchase Limits
Nothing is limited until you set a limit.
What was measured before writing this
Measured on WooCommerce 11.0.1 with a stock store:
| Question | Result |
|---|---|
| What does the core offer? | wc_customer_bought_product(), which answers yes or no and never how many. A per-customer limit exists only for coupons |
| Is there a ready table with the numbers? | wc_order_product_lookup carries product_qty and customer_id, exactly what is wanted |
| Is that table current? | No. After an order was created it still held 0 rows, with 13 jobs waiting in Action Scheduler. WooCommerce says so in its own code: correctness depends on the frequency of the AS job, and only reads it behind a filter that defaults to off |
| Does the obvious query handle refunds? | No. A customer who bought five and returned two counted as five |
| Why? | A refund is a separate order of type shop_order_refund holding a negative quantity, with an empty billing email, so it falls outside any straightforward filter |
| Do guests carry an id? | No, customer_id is 0, so they are only recognised by their billing email |
Two of those rows decide how this plugin is built.
Because the lookup table lags behind, the count is taken from the orders themselves, so a customer who has just ordered cannot immediately order again.
Because refunds sit in a separate order, the query joins back to the parent:
COALESCE( parent.billing_email, o.billing_email ) = %s
COALESCE( parent.status, o.status ) IN ( ... )
which returns the correct 3 rather than 5, in one query whatever the number of orders. A plugin that skips that join punishes customers for returning something.
What counts as bought
The same customer can mean the account, the billing email address, or both together. Both together is the default and catches the customer who signs out to order again; the account alone leaves visitors without one unlimited.
The period is a rolling number of days, the calendar month starting again on the first, or ever.
Order statuses that count are yours to tick. Processing, Completed and On hold are on by default, so a cancelled order does not eat into a limit.
Refunds are taken back off the count unless you say otherwise.
Variations either share one limit across the product, or carry a limit each.
The limits
A store wide limit applies to every product. A product may carry its own, which wins, and so may a variation while variations count on their own. A product may also be marked as never limited.
Category and tag limits are written one per line as slug | limit, and they cap the whole category taken together: offers|6 allows six items across everything in that category, not six of each. Invalid lines are dropped when you save, so what you see is what applies.
Roles can be exempted, including visitors who are not signed in.
What the customer sees
The quantity field is capped at what is left, on the classic product page and on the blocks alike, so the usual case never becomes an error. A sentence under the product says how many are left and why, and it can be replaced with your own wording using {product}, {period} and {left}.
If the cart still holds too many, because the limit was reached in another tab or the email at the checkout belongs to a customer who has bought before, the order is refused with a message. That check runs on the classic checkout and on the block checkout, using the email as it was typed on the form, not the one remembered from the session.
Customers who have reached a limit
The settings page lists the customer and product pairs at or over their limit within the current period, each linked to the product. The busiest 200 pairs of the period are examined, which is stated on the page rather than left as a silent cap.
Each row carries Let them start again. That records a date for the customer, from which their counting begins; orders placed earlier are simply left out. Nothing on any order is changed, and the fresh start can be undone.
Honest limits
- The count is of orders, not of intent. Two orders placed at the same instant in two browsers can both pass, because neither exists when the other is checked.
- Recognising a customer by email address is as reliable as the email address they type. A determined buyer with a second address counts as a second customer, whatever the setting.
- Category limits read the categories of the product, so a product moved between categories moves its history with it.
- Orders created in the admin or through the REST API are not held back. This limits the checkout a customer uses.
- The report examines the busiest 200 pairs of the period. On a very large store a customer who reached a limit on a rarely sold product may not appear, although the limit is still enforced for them.
Filters
add_filter( 'icxcnika_cpl_product_limit', function ( $limit, WC_Product $product ) {
return $limit;
}, 10, 2 );
add_filter( 'icxcnika_cpl_settings', function ( array $settings ): array {
return $settings;
} );
icxcnika_cpl_product_limit receives the resolved limit for a product, either an integer or null, and is the last word before it is enforced. icxcnika_cpl_registry_payload receives the data sent to the installation registry.
Data stored
- Option
icxcnika_cpl_settings(the rules). - Option
icxcnika_cpl_resets(the fresh starts). - Post meta on products and variations:
_icxcnika_cpl_limitand_icxcnika_cpl_exempt. - Option
icxcnika_cpl_registry_key(random installation key of 48 characters, created only if installation reporting is active). - Transient
icxcnika_cpl_release(cached GitHub release lookup, 12 hours). - Cron event
icxcnika_cpl_registry_ping(weekly, only if installation reporting is active).
No order is ever written to.
Installation reporting
On activation, on deactivation and once a week the plugin sends a small JSON message to the author's installation registry: plugin slug, plugin version, home URL, site URL, WordPress version, WooCommerce version, PHP version, locale and whether the site is a multisite. Each report carries a random installation key generated locally on first use. No customer data, order data or purchase history is ever sent.
The reporting address is empty by default, which means nothing is sent at all. Add define( 'ICXCNIKA_REGISTRY_DISABLE', true ); to wp-config.php to turn the reporting off, or define( 'ICXCNIKA_REGISTRY_URL', '...' ); to point it elsewhere.
Updates
Updates are delivered from GitHub Releases through the Update URI header of WordPress. The Plugins screen shows View details and allows automatic updates to be enabled, exactly as for a plugin from the directory.
Uninstall
Deleting the plugin removes both options, every value it wrote on products and variations, the installation key, the cached release check and the scheduled report. Orders and customers are left exactly as they were.
Customer Purchase Limits (Ελληνικά)
Περιορίζει πόσα από ένα προϊόν, μια παραλλαγή ή μια ολόκληρη κατηγορία μπορεί να αγοράσει ένας πελάτης σε ένα χρονικό διάστημα. Η μέτρηση προέρχεται από το πραγματικό ιστορικό παραγγελιών, οι επιστροφές αφαιρούνται από αυτήν, και ο πελάτης μαθαίνει πόσα του μένουν στη σελίδα προϊόντος και όχι στο ταμείο.
- Δημιουργός: George Vasiliades - https://github.com/Poseidonas
- Άδεια: GPL-2.0-or-later
- Απαιτεί WordPress 6.7 ή νεότερο
- Απαιτεί PHP 8.0 ή νεότερη
- Απαιτεί WooCommerce 9.0 ή νεότερο, δοκιμασμένο έως 11.0
- Συμβατό με το High-Performance Order Storage και με τα μπλοκ καλαθιού και ταμείου
- Οι ρυθμίσεις βρίσκονται στο WooCommerce > Όρια αγορών
Τίποτα δεν περιορίζεται μέχρι να οριστεί όριο.
Τι μετρήθηκε πριν γραφτεί
Μετρήθηκε σε WooCommerce 11.0.1 με κατάστημα χωρίς παρεμβάσεις:
| Ερώτημα | Αποτέλεσμα |
|---|---|
| Τι προσφέρει ο πυρήνας; | Το wc_customer_bought_product(), που απαντά ναι ή όχι και ποτέ πόσα. Όριο ανά πελάτη υπάρχει μόνο για κουπόνια |
| Υπάρχει έτοιμος πίνακας με τους αριθμούς; | Ο wc_order_product_lookup έχει product_qty και customer_id, ακριβώς ό,τι χρειάζεται |
| Είναι ενημερωμένος; | Όχι. Μετά τη δημιουργία παραγγελίας είχε ακόμα 0 γραμμές, με 13 εργασίες να περιμένουν στον Action Scheduler. Το γράφει το ίδιο το WooCommerce στον κώδικά του: correctness depends on the frequency of the AS job, και τον διαβάζει μόνο πίσω από φίλτρο με προεπιλογή ανενεργό |
| Χειρίζεται το προφανές ερώτημα τις επιστροφές; | Όχι. Πελάτης που αγόρασε πέντε και επέστρεψε δύο μετρήθηκε πέντε |
| Γιατί; | Η επιστροφή είναι ξεχωριστή παραγγελία τύπου shop_order_refund με αρνητική ποσότητα και κενό email χρέωσης, οπότε πέφτει έξω από κάθε απλό φίλτρο |
| Έχουν οι επισκέπτες ταυτότητα; | Όχι, το customer_id είναι 0, οπότε αναγνωρίζονται μόνο από το email χρέωσης |
Δύο από αυτές τις γραμμές καθορίζουν πώς είναι χτισμένο το πρόσθετο.
Επειδή ο πίνακας αναζήτησης καθυστερεί, η μέτρηση παίρνεται από τις ίδιες τις παραγγελίες, ώστε ένας πελάτης που μόλις παρήγγειλε να μην μπορεί να παραγγείλει αμέσως ξανά.
Επειδή η επιστροφή βρίσκεται σε ξεχωριστή παραγγελία, το ερώτημα ενώνεται πίσω με τη γονική:
COALESCE( parent.billing_email, o.billing_email ) = %s
COALESCE( parent.status, o.status ) IN ( ... )
που επιστρέφει το σωστό 3 αντί για 5, με ένα ερώτημα ανεξάρτητα από το πλήθος των παραγγελιών. Ένα πρόσθετο που παραλείπει αυτή την ένωση τιμωρεί τους πελάτες επειδή επέστρεψαν κάτι.
Τι μετράει ως αγορασμένο
Ο ίδιος πελάτης μπορεί να σημαίνει τον λογαριασμό, τη διεύθυνση email χρέωσης, ή και τα δύο μαζί. Τα δύο μαζί είναι η προεπιλογή και πιάνουν τον πελάτη που αποσυνδέεται για να παραγγείλει ξανά· μόνο ο λογαριασμός αφήνει απεριόριστους όσους δεν έχουν.
Το διάστημα είναι κυλιόμενος αριθμός ημερών, ο ημερολογιακός μήνας με νέο ξεκίνημα την πρώτη, ή συνολικά.
Οι καταστάσεις παραγγελίας που μετράνε επιλέγονται από εσάς. Οι Σε επεξεργασία, Ολοκληρωμένη και Σε αναμονή είναι ενεργές από την αρχή, ώστε μια ακυρωμένη παραγγελία να μην τρώει από το όριο.
Οι επιστροφές αφαιρούνται από τη μέτρηση εκτός αν ορίσετε αλλιώς.
Οι παραλλαγές είτε μοιράζονται ένα όριο σε όλο το προϊόν, είτε έχουν από ένα η καθεμιά.
Τα όρια
Ένα καθολικό όριο ισχύει για κάθε προϊόν. Ένα προϊόν μπορεί να έχει δικό του, που υπερισχύει, και το ίδιο και μια παραλλαγή όσο οι παραλλαγές μετρώνται χωριστά. Ένα προϊόν μπορεί επίσης να σημειωθεί ως χωρίς όριο ποτέ.
Τα όρια κατηγοριών και ετικετών γράφονται ένα ανά γραμμή ως slug | όριο, και περιορίζουν ολόκληρη την κατηγορία μαζί: το offers|6 επιτρέπει έξι τεμάχια συνολικά από όλα όσα υπάρχουν εκεί, όχι έξι από το καθένα. Οι άκυρες γραμμές αφαιρούνται κατά την αποθήκευση, ώστε αυτό που βλέπετε να είναι αυτό που ισχύει.
Ρόλοι μπορούν να εξαιρεθούν, συμπεριλαμβανομένων των επισκεπτών χωρίς σύνδεση.
Τι βλέπει ο πελάτης
Το πεδίο ποσότητας περιορίζεται σε ό,τι απομένει, τόσο στην κλασική σελίδα προϊόντος όσο και στα μπλοκ, ώστε η συνηθισμένη περίπτωση να μη γίνεται ποτέ σφάλμα. Μια πρόταση κάτω από το προϊόν λέει πόσα μένουν και γιατί, και μπορεί να αντικατασταθεί με δική σας διατύπωση με τα {product}, {period} και {left}.
Αν το καλάθι εξακολουθεί να έχει υπερβολικά πολλά, επειδή το όριο συμπληρώθηκε σε άλλη καρτέλα ή επειδή το email στο ταμείο ανήκει σε πελάτη που έχει ξανααγοράσει, η παραγγελία απορρίπτεται με μήνυμα. Ο έλεγχος τρέχει στο κλασικό ταμείο και στο ταμείο με μπλοκ, με το email όπως γράφτηκε στη φόρμα, όχι με αυτό που θυμάται η συνεδρία.
Πελάτες που έχουν φτάσει σε όριο
Η σελίδα ρυθμίσεων δείχνει τα ζεύγη πελάτη και προϊόντος που βρίσκονται στο όριό τους ή το έχουν ξεπεράσει μέσα στο τρέχον διάστημα, με σύνδεσμο στο προϊόν. Εξετάζονται τα 200 πιο πολυάριθμα ζεύγη του διαστήματος, κάτι που δηλώνεται στη σελίδα αντί να μείνει σιωπηλός περιορισμός.
Κάθε γραμμή φέρει το Άφησέ τον να ξεκινήσει ξανά. Αυτό καταγράφει μια ημερομηνία για τον πελάτη, από την οποία αρχίζει η μέτρησή του· οι προγενέστερες παραγγελίες απλώς μένουν εκτός. Τίποτα σε καμία παραγγελία δεν αλλάζει, και το νέο ξεκίνημα μπορεί να αναιρεθεί.
Τίμιοι περιορισμοί
- Η μέτρηση αφορά παραγγελίες, όχι προθέσεις. Δύο παραγγελίες που γίνονται την ίδια στιγμή σε δύο παράθυρα μπορούν να περάσουν και οι δύο, γιατί καμία δεν υπάρχει όταν ελέγχεται η άλλη.
- Η αναγνώριση πελάτη από email είναι τόσο αξιόπιστη όσο και το email που πληκτρολογεί. Ένας αποφασισμένος αγοραστής με δεύτερη διεύθυνση μετράει ως δεύτερος πελάτης, ό,τι ρύθμιση και να έχετε.
- Τα όρια κατηγορίας διαβάζουν τις κατηγορίες του προϊόντος, οπότε ένα προϊόν που μετακινείται σε άλλη κατηγορία παίρνει μαζί του το ιστορικό του.
- Παραγγελίες που δημιουργούνται στη διαχείριση ή μέσω REST δεν εμποδίζονται. Αυτό περιορίζει το ταμείο που χρησιμοποιεί ο πελάτης.
- Η αναφορά εξετάζει τα 200 πιο πολυάριθμα ζεύγη του διαστήματος. Σε πολύ μεγάλο κατάστημα, ένας πελάτης που έφτασε όριο σε σπάνια πωλούμενο προϊόν μπορεί να μην εμφανιστεί, παρότι το όριο εξακολουθεί να ισχύει για αυτόν.
Φίλτρα
add_filter( 'icxcnika_cpl_product_limit', function ( $limit, WC_Product $product ) {
return $limit;
}, 10, 2 );
add_filter( 'icxcnika_cpl_settings', function ( array $settings ): array {
return $settings;
} );
Το icxcnika_cpl_product_limit δέχεται το τελικό όριο ενός προϊόντος, ακέραιο ή null, και έχει τον τελευταίο λόγο πριν εφαρμοστεί. Το icxcnika_cpl_registry_payload δέχεται τα δεδομένα που στέλνονται στο μητρώο εγκαταστάσεων.
Δεδομένα που αποθηκεύονται
- Option
icxcnika_cpl_settings(οι κανόνες). - Option
icxcnika_cpl_resets(τα νέα ξεκινήματα). - Post meta σε προϊόντα και παραλλαγές:
_icxcnika_cpl_limitκαι_icxcnika_cpl_exempt. - Option
icxcnika_cpl_registry_key(τυχαίο κλειδί εγκατάστασης 48 χαρακτήρων, δημιουργείται μόνο αν είναι ενεργή η αναφορά εγκατάστασης). - Transient
icxcnika_cpl_release(cache της αναζήτησης έκδοσης στο GitHub, 12 ώρες). - Cron event
icxcnika_cpl_registry_ping(εβδομαδιαίο, μόνο αν είναι ενεργή η αναφορά εγκατάστασης).
Σε καμία παραγγελία δεν γράφεται τίποτα.
Αναφορά εγκατάστασης
Κατά την ενεργοποίηση, την απενεργοποίηση και μία φορά την εβδομάδα το πρόσθετο στέλνει ένα μικρό μήνυμα JSON στο μητρώο εγκαταστάσεων του δημιουργού: slug και έκδοση του πρόσθετου, home URL, site URL, έκδοση WordPress, έκδοση WooCommerce, έκδοση PHP, γλώσσα και αν το site είναι multisite. Κάθε αναφορά συνοδεύεται από ένα τυχαίο κλειδί εγκατάστασης που δημιουργείται τοπικά την πρώτη φορά. Δεδομένα πελατών, παραγγελιών ή ιστορικού αγορών δεν στέλνονται ποτέ.
Η διεύθυνση αναφοράς είναι κενή από προεπιλογή, που σημαίνει ότι δεν αποστέλλεται τίποτα. Προσθέστε define( 'ICXCNIKA_REGISTRY_DISABLE', true ); στο wp-config.php για να απενεργοποιήσετε την αναφορά, ή define( 'ICXCNIKA_REGISTRY_URL', '...' ); για να τη στείλετε αλλού.
Ενημερώσεις
Οι ενημερώσεις παραδίδονται από τα GitHub Releases μέσω της κεφαλίδας Update URI του WordPress. Η οθόνη Πρόσθετα δείχνει Προβολή λεπτομερειών και επιτρέπει την ενεργοποίηση αυτόματων ενημερώσεων, ακριβώς όπως για πρόσθετο του καταλόγου.
Απεγκατάσταση
Η διαγραφή του πρόσθετου αφαιρεί και τα δύο options, κάθε τιμή που έγραψε σε προϊόντα και παραλλαγές, το κλειδί εγκατάστασης, τον αποθηκευμένο έλεγχο έκδοσης και το προγραμματισμένο ραντεβού. Παραγγελίες και πελάτες μένουν ακριβώς όπως ήταν.
Releases
1 release. Each count is every asset in that release; expand a row for the breakdown.