augustash / mna_offers
Schedules the offers on Minnesota Air dealer sites from one screen and imports the coupons already on their pages.
Package info
github.com/augustash/drupal-mna-offers
Type:drupal-module
pkg:composer/augustash/mna_offers
Requires
- drupal/core: ^10.3 || ^11
- drupal/entity_reference_revisions: ^1.10
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 16:57:48 UTC
README
Scheduled offers for Minnesota Air dealer sites. Dealer staff manage every discount and promotion from Content › Offers, with start and end dates that put each one up and take it down. August Ash migrates the offers already typed into a site's pages from Configuration › Content › Offers setup, and the pages look exactly as they did.
How it works
An offer does not store its own wording, image or button. It owns one of the site's existing offer components and adds what those never had: dates, one list, and a record of which pages show it.
| Site generation | An offer's content is | Placed on a page by |
|---|---|---|
| Paragraphs (37 sites) | a coupon paragraph |
an "Offers" paragraph (mna_offers_slot) |
| eXo / Layout Builder (11 sites) | a mna_theme_offer block |
an "Offers" block (mna_offers_slot) |
Rendering through the site's own component is what makes an offer identical to the coupon it replaced: the theme's own template, the hardcoded dealer phone number, the Schedule Service webform modal, eXo's wrapper, modifiers and first/last markers. There are no offer templates in this module to drift out of step with 48 themes.
The offer form is built the same way: the component's own fields, with the site's own widgets, and on eXo sites the component's Appearance choices (Background Color) from eXo's property manager, exactly as Layout Builder offers them. Paragraphs coupons have no appearance settings to carry; their theme colours every coupon alike.
On Paragraphs sites the "Offers" paragraph is never rendered itself: before a
page is built, it is swapped in memory for the coupons of the offers running
today, in its position in the field. Themes that read the field directly (one
sizes a Coupons section with a with-2-coupons class counted from its items)
see exactly what they saw before migration. The swap is undone as soon as the
page has rendered, and before any save, so it is never stored. On eXo sites the
"Offers" block is replaced in the layout build in hook_node_view(), which core
runs before eXo's hook_entity_view() marks each region's first and last
component.
The generation is detected, not configured. mna_offers.adapter returns the
ParagraphCouponAdapter or ExoOfferAdapter, and everything generation
specific (form, rendering, finding and replacing components on a page) lives
behind ComponentAdapterInterface.
Migration
OfferImporter walks each page's current revision for components. Parent
pointers are not trusted: on the surveyed fleet 1 in 10 coupon paragraphs was
still on a page, and the rest were leftovers from old revisions that must not
come back as live offers.
- Adjacent components on a page become one placement; an offer remembers its placement, so offers on the homepage stay on the homepage.
- Printed expiry lines ("Ends November 20th, 2026", "EXP. 7/31/17") become end dates. A line with no year uses the year it was written. A line with no date keeps showing and is flagged. Sites without an expiry field are read for a sentence like "Offer ends 11/20/2026" in the description.
- Components are copied, not moved. The page is saved as a new revision and "Return offers to this page" puts components back from the offers as they are now. Offers not showing today go back unpublished, and offers whose spot is no longer on the page are left alone rather than deleted.
- Offers already past their printed date keep showing, as they do today, and are flagged for the dealer, unless the reviewer ticks "Take down" for them. Migrating never changes what visitors see on its own.
- A component hidden on its page becomes an offer that is turned off, so "Turn on" shows it. Identical components on several pages share one offer only when they would also show the same way.
- Migrating a page again, after someone types a new coupon into it, adds to its record, so "Return" still puts back everything.
- Components are copied without File (Field) Paths moving their files, so image URLs stay where they are.
Migration creates content only, so it runs on live through the UI even though these sites lock configuration for web requests.
Visibility and caching
PlacementRenderer loads the running offers for a placement and gives the
build a max-age of seconds until the site's midnight plus the mna_offer_list
tag. hook_cron() clears that tag once per day for the page cache and the
Pantheon edge, which only respond to tags. End dates are inclusive and use the
site timezone; the setup screen warns when a site is on UTC.
Screens
Dealer staff reach the Offers screen from the site's admin bar: Manage Offers in Escort on Paragraphs sites, and in the eXo toolbar's Content group on eXo sites. The module ships both items as optional config, so each site gets the one for the admin bar it has, and only users who may open the screen see it.
| Path | Who | What |
|---|---|---|
/admin/content/offers |
Dealer staff (manage mna offers) |
Season strip, offers grouped by status, turn on/off |
/admin/content/offers/add, /{id}/edit |
Dealer staff | The site's own component fields plus when, where and on/off, with a live preview |
/admin/config/content/offers |
August Ash (administer mna offers) |
What is particular about this site |
/admin/config/content/offers/migrate |
August Ash | Review and migrate pages, return migrated pages |
Install
Needs Drupal 10.3 or 11 and either a coupon paragraph type or the eXo Offer
component. The module refuses to install without one.
- Add the package, on a local copy of the site with dev's database:
composer require augustash/mna_offers:^1.0. It comes from Packagist and lands inweb/modules/contrib/mna_offers, which every site's.gitignorealready covers. - Export its config locally:
drush cim -yfirst, so the export holds only what the module adds, thendrush en mna_offers -yanddrush cex -y. Commitcomposer.json,composer.lockandconfig/. These sites lock config on the web and Pantheon stays in git mode, so config only reaches an environment through git. - Deploy: push to dev, then deploy to test and live, and on each run
drush updb -y && drush cim -y && drush cr. - Migrate the offers: on each environment open Configuration › Content › Offers setup › Migrate, check the ticked pages and migrate them.
- Hand over: dealer staff manage offers from Manage Offers in the admin bar.
On Pantheon, run drush as terminus remote:drush <site>.<env> -- <command>.
Tests
From web/ on a Paragraphs site (the kernel tests need paragraphs), with
SQLite so the site's own database is never touched:
SIMPLETEST_DB=sqlite://localhost//tmp/mna-offers.sqlite \ ../vendor/bin/phpunit -c core modules/contrib/mna_offers/tests
The parser tests include every expiration line found on a live page across the fleet in September 2026. The kernel tests cover dates, migrating and returning a page, and a page keeping its Offers spot through a render and a save.