Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
It would be very helpful to allow for bulk creation of coupons. The requirements I would have for this are:
- Select number of coupons to generate
- Custom prefix
- Export of coupons, by Promotion
If #2902880: Move coupons to a new tab is the best route than this issue is blocked by it.
Comment | File | Size | Author |
---|---|---|---|
#50 | bulk coupon generation.png | 75.08 KB | bojanz |
#45 | bulk-coupons-2902882-45.patch | 27.76 KB | lisastreeter |
| |||
#45 | interdiff-2902882-40-45-do-not-test.diff | 5.51 KB | lisastreeter |
#15 | bulk-coupons-2902882-15.patch | 24.65 KB | lisastreeter |
|
Comments
Comment #2
chrisrockwell CreditAttribution: chrisrockwell commentedComment #3
chrisrockwell CreditAttribution: chrisrockwell commentedI have a functioning port of commerce_coupon_batch, assuming this functionality would continue as a contrib (not sure of that) https://www.drupal.org/project/issues/commerce_coupon_batch
Comment #4
bojanz CreditAttribution: bojanz at Centarro commentedWhy would it be a contib if we have this feature request for it?
Comment #5
chrisrockwell CreditAttribution: chrisrockwell commentedIf you decided it should be a contrib instead of an included feature :D. I just wasn't sure and didn't want to bother you all with it until I had something working, and I need it now, so I'm using it as a contrib. If you're up for bringing it into Commerce I'd be happy to do that.
https://github.com/clrockwell79/commerce_coupon_batch
In general does it look sane with the action, form, and BatchCoupon class?
Is there a better way to include additional configuration that could one day be on the Coupon entity? I guess I could just use the children of the render display to inform the save instead of having to hardcode things like usage_limit.
Right now it's effectively bypassing CouponAccessControlHandler (I think) so I'd need to do something about that
Comment #6
lisastreeter CreditAttribution: lisastreeter at Centarro commentedThis initial patch has a form implementation but no batch and no tests yet. I will add test code incrementally and then start working on the batch functionality.
Comment #7
lisastreeter CreditAttribution: lisastreeter at Centarro commentedAdded kernel test for coupon code provider service.
Comment #8
lisastreeter CreditAttribution: lisastreeter at Centarro commentedMisplacement of file and syntax error fixed.
Comment #9
lisastreeter CreditAttribution: lisastreeter at Centarro commentedMissing order type config.
Comment #10
lisastreeter CreditAttribution: lisastreeter at Centarro commentedStill missing modules/schema. Added others.
Comment #11
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #12
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #13
lisastreeter CreditAttribution: lisastreeter at Centarro commentedAdded batch processing to the form. Still need testing for the form.
Comment #14
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #15
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #16
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #17
lisastreeter CreditAttribution: lisastreeter at Centarro commentedAdded functional test code for bulk coupon generation (CouponTest).
Comment #19
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #20
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #21
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #22
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #23
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #24
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #25
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #26
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #27
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #28
lisastreeter CreditAttribution: lisastreeter at Centarro commentedReduced coupon quantity.
Comment #29
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #30
drugan CreditAttribution: drugan as a volunteer commented@lisastreeter
Does this test pass on your local install?
Comment #31
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #32
lisastreeter CreditAttribution: lisastreeter at Centarro commented@drugan Sorry to clog up the issue with all my failures!
Unfortunately, I don't have local testing set up for myself yet. (I'm still new to Drupal development.) But at the moment, I'm putting it off until after an upcoming training session (related to that) I've registered for at MidCamp. So for now, I'm just using the test server here to test patches.
This is my first time working on a Functional test, and my first time implementing a batch in a form, so there have been lots of little bugs as I'm learning as I go. I think I'm getting close. Hope so, at least.
Comment #33
drugan CreditAttribution: drugan as a volunteer commentedOK, if the #31 will fail I'll try to debug the patch on my local install.
Comment #34
lisastreeter CreditAttribution: lisastreeter at Centarro commented@drugan Success!! Thank you for the support and offer to help. I appreciate it. Last night the big thing I was stuck on was getting drupalPostForm to work. After that, it was just little things. Gotta get my local dev environment properly set up soon!
Comment #35
drugan CreditAttribution: drugan as a volunteer commentedCongrats!
Yes, running FunctionalJavascript tests might be seen as a bit tricky. Feel free to ping me on the question if you'd have some troubles with it.
Comment #36
drugan CreditAttribution: drugan as a volunteer commentedOops..
Comment #37
bojanz CreditAttribution: bojanz at Centarro commentedGreat work so far!
And now my favorite hobby, nitpicking naming :)
We tend to use NounVerbForm, so we want CouponGenerateForm and entity.commerce_promotion_coupon.generate_form.
Is this valid grammar-wise? I've only seen "Bulk generate" before (VS "generate bulk").
You are serializing an entire entity into the batch, which is very expensive. Would be much better to pass the three values in an array.
You should not use camelCase naming for local variables.
We're missing sensible defaults for $prefix, $suffix, $length ($prefix and $suffix can be empty strings, $length should be the default from the form). We're also missing code that validates $type and throws an \InvalidArgumentException if the value is unknown. I've explained below that we should have the possible types as constants on the value object.
"Provider" is a very generic name, currently only used for the CartProvider in Commerce.
I would go for "CouponCodeGeneratorInterface", cause the primary purpose of the class is to generate coupon codes.
That also matches your form naming. Your docblock can then be more concrete as well:
"Generates coupon codes (unique, machine readable coupon identifiers)."
or
"Generates coupon codes (unique, machine readable identifiers for coupons)."
Having this method here is odd, I'd expect the CouponCodePattern to have constants for the three types (CouponCodePattern::TYPE_NUMERIC, etc). Then the form itself would provide the labels for each constant.
"Gets" is for getters, this isn't one, it has real logic.
I'd say "Validates the given pattern for the specified quantity." and call it validatePattern($pattern, $quantity = 1);
The verb in the method name should match the verb in the docblock. We have generates/get here. Since "get" is used for getting properties, we tend to avoid it in methods that do something, so this method should be generateCode. Furthermore, I'd argue that we actually want generateCodes(CouponCodePattern $pattern, $quantity = 1), cause that would allow for performance optimizations in the method itself. More on that later.
Each codeExists() call runs a DB query. So we're running up to a thousand queries per batch.
We need to pass a $quantity to this method, generate the codes all at once, then check their uniqueness all at once.
I suggest limiting the batch to 25 codes per run. Then, this method generates $quantity * 2 codes, checks their uniqueness at once, removes all non-unique codes, and returns the first $quantity ones. The form could also check the total number of generated codes and suggest that a prefix/suffix is used the next time.
This method needs an explanation for why not every letter/number is used (just like the D7 code had).
Comment #38
lisastreeter CreditAttribution: lisastreeter at Centarro commentedPatch provides changes in response to code review #37 (@bojanz)
Comment #39
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #40
lisastreeter CreditAttribution: lisastreeter at Centarro commentedRemoved unused use statements and cleaned up a few after things after reviewing interdiff.
Comment #41
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #42
bojanz CreditAttribution: bojanz at Centarro commentedThanks! This looks almost ready.
Why did we switch from an EntityQuery to a database query? Was there a problem with the IN condition there?
Also, let's look at the UI:
1) The generate form is rendered in the frontend theme, which is not consistent with the other promotion pages which all use the backend theme. You are missing a _admin_route: TRUE on the route.
2) "Number of coupons to generate?" -> we don't use question marks in field labels. The field could use a default value (10? 20? 30? what do the others do?), and a longer length, see how it's not the same length as the "Code length" field.
3) "Code format" should have a value (alphanumeric?) preselected.
Comment #43
mglamanSince the route parameter matches an entity type ID, we do not need to explicitly define our options.
I wonder if this should actually be based on `\Drupal\Core\Form\ConfirmFormBase`.
The confirmation form would allow us to say
* Here's an example coupon code
* You will be making ### coupons that are (Enabled|Disabled) and can be used (X times|unlimited times)
We need to pass these through `t()` still.
Let's add a description to help clarify this.
"Number of times each generated coupon can be used"
Comment #44
lisastreeter CreditAttribution: lisastreeter at Centarro commented@bojanz @mglaman Thanks for the code review. A quick response to a couple items before I create the patch for the updated UI items.
The IN condition was fine, but I needed the codes, not the coupon ids. That's why I switched to EntityQuery. (I also tried using a static instead of dynamic query but there was no noticeable performance difference. So I left it in the easier-to-read format.)
I made the change but then submitForm didn't work. For some reason, without the explicit parameter option, the promotion was never set in the constructor.
Comment #45
lisastreeter CreditAttribution: lisastreeter at Centarro commentedUI Improvements.
Comment #46
lisastreeter CreditAttribution: lisastreeter at Centarro commentedComment #47
bojanz CreditAttribution: bojanz at Centarro commentedLast round!
StringTranslationTrait is not used anywhere.
a. The comment needs to explain $quantity * 2. Something like "Generate twice the requested quantity, to improve chances of having the needed quantity after removing non-unique/existing codes."
b. $rand_index should be $random_index, we rarely shorten words.
c. Having nested for loops is a bit hard on the eyes, I'd suggest rewriting the inner one as:
We should be using the defined constants for the pattern type matching.
A more common constant/variable name would be BATCH_SIZE.
Injecting $entity_field_manager and looking at the field definition feels like a lot of work just to avoid breaching the 255 char limit. I'd rather hardcode it in a constant for simplicity.
We can replace this check, and the constant with a #max on the quantity field, no?
Note that the batch is given batchAddCoupons, then batchFinished, but in code, the methods are defined in reverse, batchFinished comes before batchAddCoupons. I'd change the method ordering to match the order in which they're given to the batch. I'd also rename the methods to follow verbNoun, which is more common, so processBatch() and finishBatch().
You don't need a base coupon at all, just pass ['code' => $code] + $coupon_values when creating the coupon further down.
Translated messages that depend on a count need to use \Drupal::translation()->formatPlural().
Comment #48
bojanz CreditAttribution: bojanz at Centarro commentedI'll wrap this up.
Comment #50
bojanz CreditAttribution: bojanz at Centarro commentedHouston, we have bulk coupon generation :)
Comment #51
lisastreeter CreditAttribution: lisastreeter at Centarro commented@bojanz Thank you for the detailed description of the last of the code issues. It's very helpful. Thank you also for making the fixes.
Regarding this comment:
If I use #max on the quantity field, it doesn't allow me to set #length on the quantity field; instead, it sets it to a size just big enough to accommodate the 4 digit maximum. Which makes the UI look a little off, since the size of that text box doesn't match the size of all the others on the page.
Comment #53
fotograafinge CreditAttribution: fotograafinge commentedThis worked great. Thanks for the hard work.
But it doesn't work any longer (commerce 2.8). I get this error:
An AJAX HTTP error occurred.
HTTP Result Code: 200
Debugging information follows.
Path: /batch?id=6233&op=do_nojs&op=do
StatusText: OK
ResponseText: Error: Call to a member function getPromotionId() on null in Drupal\commerce_promotion\Entity\Promotion->postSave() (line 505 of /var/www/html/web/modules/contrib/commerce/modules/promotion/src/Entity/Promotion.php).
Someone having the same problem
(do I need to make a new issue for this?)