View Categories

Programming Notes

25 min read

These notes are for developers who extend, integrate or debug the Magento 2 rental extension. They describe the current release, releaserental2 1.2.205, on Magento 2.4.8 and 2.4.9 with PHP 8.3 or 8.4. For what changed in each version, see the Release Notes.

One rule comes before everything else: never edit the code under vendor/salesigniter/. Composer replaces it on every update. Put your changes in your own module, sequenced after ours, and use plugins, observers and layout XML rather than copies of our classes.

The modules and where they come from #

Everything is installed with Composer from five licence-gated feeds. Each feed has its own host name, and Composer needs a separate http-basic entry for each host you use: the username is the feed name and the password is your licence key. The install pages generate the exact commands for your licence: Regular, then Pro on top of it, and Multi Source Inventory on top of that. To pin a version, see Installing a Specific Version.

FeedRepository URLServes
regularhttps://rental.rentalbookingsoftware.com/composer/regularThe core extension and its free companions
prohttps://rentalpro.rentalbookingsoftware.com/composer/proContracts, maintenance, extensions, deposits and damage waiver, order editing
msihttps://rentalmsi.rentalbookingsoftware.com/composer/msiThe Multi Source Inventory integration
rfqhttps://packagerfq.rentalbookingsoftware.com/composer/rfqThe Amasty RFQ integration
vendorhttps://rentalvendor.rentalbookingsoftware.com/composer/vendorThe multi-vendor marketplace integration
Composer packageMagento moduleFeedWhat it isRequires
salesigniter/releaserental2SalesIgniter_RentalregularThe core: the sirent product type, calendar, pricing, reservations, reports, send and return, reminders, iCal feeds and imports, GraphQL APIreleasecommon2, bookingsgrid
salesigniter/releasecommon2SalesIgniter_CommonregularShared helpers and the salesigniter:* maintenance commands1.2.54 or later on Magento 2.4.9
salesigniter/bookingsgridSalesIgniter_BookingsgridregularCustomer and admin bookings gridsreleaserental2
salesigniter/hyvarentalHyva_SalesIgniterRentalregularHyvä companion: add-to-cart button, pricing card and out-of-stock modal. Hyvä stores only.releaserental2 1.2.197+, hyva-themes/magento2-compat-module-fallback
salesigniter/releaserentalcontract2SalesIgniter_RentalContractproRental contracts and signaturesreleaserental2 1.2.200+
salesigniter/releasemaintenance2SalesIgniter_MaintenanceproMaintenance tickets, which take stock off salereleaserental2 1.2.198+
salesigniter/releaserentalextend2SalesIgniter_RentalextendproCustomers extend a rental that is already bookedreleaserental2, releasecommon2
salesigniter/waiverdamageSalesIgniter_WaiverDamageproRefundable security deposits and damage waiverreleaserental2 1.2.197+
salesigniter/ordereditintegrationSalesIgniter_OrdereditproMageWorx Order Editor integrationreleaserental2, releasecommon2
salesigniter/releasepurchaserentals2SalesIgniter_PurchaserentalsproSpecial pricing for products that are on rentreleaserental2
salesigniter/releaserentalinventory2SalesIgniter_RentalinventorymsiMSI: rental stock per source1.2.47 or later on Magento 2.4.9
salesigniter/releaserfqintegration2SalesIgniter_RfqintegrationrfqAmasty Request a Quote integrationreleaserental2
salesigniter/marketintegration, marketcustom, customvendorpdfSalesIgniter_Marketintegration and friendsvendorVnecoms multi-vendor marketplace integrationreleaserental2, releasecommon2

The regular feed also carries small bridges for specific third-party modules: partialpay and partialpayshow (Milople Partial Payment), preventstockdeduction (stops MSI source deduction for rentals), cartdates, pickupdropoffdates, releasepdfserial, fixedaddress, mageplazaintegration and preorderfix. Install one only if you run the module it bridges.

composer show "salesigniter/*" lists what an install actually has. The package ships its own design notes in vendor/salesigniter/releaserental2/docs/ (UPGRADING.md, hyva.md, template-plugin-removal.md, calendar-page-cache.md, inventory-rewrite.md, ical-import-architecture.md). They go deeper than this page.

Starting your own module #

Declare a sequence on our module so your plugins, layout and DI load after ours, and require it in your composer.json:

<!-- app/code/Acme/Rental/etc/module.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Acme_Rental">
        <sequence>
            <module name="SalesIgniter_Rental"/>
        </sequence>
    </module>
</config>

How the extension is built #

The sirent product type and its attributes #

Rental products are product type sirent (labelled “Reservation” in the admin), with SalesIgniter\Rental\Model\Product\Price as the price model. A sirent product can be a bundle selection or a configurable child. A bundle or configurable becomes a rental through the sirent_rental_type attribute. To ask “is this a rental?”, call SalesIgniter\Rental\Helper\Data::isRentalType($product) rather than comparing type ids, because it handles all three cases.

Every rental product carries four custom options: Start Date:, End Date:, Rental Buyout: and Damage Waiver:. They are created for you on save, including for products created in code through ProductRepositoryInterface::save() (plugin Plugin\Product\EnsureRentalCustomOptions). They are hidden on the storefront, but they are what carries the dates through the quote, the order, invoices and emails. Never delete them.

AttributeHolds
sirent_quantityHow many units the shop owns. StockManagementInterface::getSirentQuantity() reads it (per source on MSI).
sirent_rental_typeWhether a bundle or configurable uses the rental calendar
sirent_pricingtype, sirent_priceThe pricing type and the rental price grid. Rows live in sirental_price and are saved by Model\Attribute\Backend\RentalPrice.
sirent_use_times, sirent_hotel_modeTime of day on the calendar; hotel-style nights
sirent_min*, sirent_max*, sirent_turnover_before*, sirent_turnover_after*, sirent_padding*Minimum and maximum period, turnover time and padding
sirent_excluded_days*, sirent_excluded_dates, sirent_future_limitDays and dates that cannot be booked, and how far ahead a customer may book
sirent_enable_buyout, sirent_buyout_priceBuy the item instead of renting it
sirent_damage_waiver, sirent_depositDamage waiver and deposit amounts
sirent_serial_numbers_useTrack individual units by serial number
sirent_always_showPer-product override of the date picker style (range picker or separate fields)
sirent_allow_overbookingPer-product overbooking flag. The storefront refusal uses the store setting salesigniter_rental/inventory/allow_overbooking.

-200 means “use the store setting” #

Helper\Data::USE_CONFIG_DEFAULT is -200. It is stored in a product’s own attribute row to mean “fall back to Stores > Configuration > Sales Igniter > Rental“. It turns up in the period, turnover, padding, excluded-days, fixed-length and damage waiver attributes. So a plain SELECT can make a product look as if it has a damage waiver of minus two hundred. It doesn’t. Don’t “clean up” these rows, and treat an empty value and -200 the same way when you read one. The option list for sirent_fixed_type even includes -200, only so that product imports carrying the sentinel don’t fail.

Reservations: sirental_reservationorders #

Every booking, hold and block is a row in sirental_reservationorders. It is the single source of truth for availability.

ColumnMeaning
order_id, order_item_id, order_increment_idThe order line. order_id = 0 means there is no order: a manual reservation, a maintenance block or an iCal import. That is why the foreign key to sales_order is declared disabled.
product_id, qty, qty_cancelWhat is reserved and how much, less anything cancelled
start_date, end_dateThe dates the customer chose
start_date_with_turnover, end_date_with_turnoverThe same dates widened by turnover time
start_date_use_grid, end_date_use_grid, qty_use_gridWhat availability actually reads. A row with qty_use_grid = 0 blocks nothing.
order_typeorder, manual, maintenance, rfq or ical
source_codeThe MSI source. NULL counts against every stock.
qty_shipped, qty_returned, serials_shipped, serials_returnedSend and return. Overdue reminders are decided from these counters.
historyA log of what touched the row. Read it first when a row looks wrong.

Rows are written the first time an order is saved (plugin Plugin\Sales\Api\OrderRepositoryInterface::afterSave() on the order resource model), when Reserve inventory is set to “With No Invoice (Recommended)”. With “When Invoiced” they are written when the invoice is paid (Observer\InvoicePay). With a specific status selected, they are written when the order reaches that status. Cron\AsyncInventoryUpdate runs every 15 minutes as a reconciler. It should find nothing. When it does find and reserve an order, it logs a WARNING naming that order. Treat that as a bug to investigate, usually an exception during placement or an order created by code that skips the order resource save.

Availability is derived on read #

Nothing is cached in a table or a product attribute. Every availability question runs the same path:

  1. Model\Inventory\ReservationQuery fetches the live rows for a product and a date window.
  2. Model\Inventory\InventoryArrayBuilder turns them into non-overlapping intervals with a sweep line, summing quantity per segment.
  3. Model\Inventory\Availability answers “booked quantity”, “fully booked days” and “first date available” from those intervals, memoised for the request.
  4. Model\StockManagement (Api\StockManagementInterface) is the public face every caller uses: the calendar, add to cart, checkout validation, the admin reports, GraphQL and the add-ons.

The old salesigniter_rental cache type, its cron job and its console command were removed in 1.2.197. If a deploy still references UpdateRentalCache, clear generated/.

Some quantities never refuse a booking. Unmanaged stock is reported as 99999, and Model\Calendar\Rules::UNMANAGED_QTY treats anything at or above it as unlimited. An admin closure is written as an interval of Stock::OVERBOOK_QTY (9999999) and blocks the dates regardless. Turnover is applied by shifting intervals, not by rebuilding them.

The browser decides what to offer, the server decides what to accept. The rules exist twice, in Model/Calendar/Rules.php and in view/base/web/js/flatpickr/sibookingCalendarRules.js, and shared case tables in Test/fixtures/ are asserted from both sides. If you change one side, change the other too. Otherwise the calendar will offer dates that add to cart then refuses.

Dates are stored as MySQL wall-clock strings #

Since 1.2.196, rental dates are stored as naive store wall-clock datetimes in MySQL format, Y-m-d H:i:s. No time zone conversion happens between the calendar and the database. The time the customer picked is the time stored in sirental_reservationorders, in the quote and order item options, and in the buy request. A whole-day rental runs from 00:00:00 on the first day to 23:59:00 on the last. Model\RentalDates is the value object and reads every legacy shape.

info_buyRequest.calendar_selector = {
    "from": "2026-06-23 00:00:00",
    "to": "2026-06-25 23:59:00",
    "locale": "en_US",
    "format": "mysql"
}

For display, go through RentalDateFormatter, which formats the stored value as UTC, which means verbatim. Two mistakes to avoid. Don’t convert rental dates from UTC the way you would Magento’s own created_at, which really is UTC. And don’t zero the time on an end date: that ends every whole-day rental at midnight at the start of its last day, and the calendar frees it a day early.

Stores upgrading from an older version get a one-way, idempotent conversion. A data patch schedules it, Cron\MigrateDates runs it in chunks on the next cron pass, and nothing runs inside setup:upgrade. To run it by hand:

bin/magento salesigniter:rental:migrate-dates --dry-run
bin/magento salesigniter:rental:migrate-dates
# narrower runs
bin/magento salesigniter:rental:migrate-dates --orders-only --limit=500
bin/magento salesigniter:rental:migrate-dates --item-id=1234 --item-id=1235

Rows it cannot parse are reported and left untouched.

Extension points #

Service contracts #

InterfaceUse it to
Api\StockManagementInterfacegetAvailableQuantity($product, $start, $end); checkIntervalValid($product, $dates, $qty); getSirentQuantity($product); getFirstDateAvailable($product); getInventoryArray($product); reserveQuoteOrOrder($order); saveReservation(), cancelReservationQty(), deleteReservationsByOrderId()
Api\ReservationOrdersRepositoryInterfaceRead and save reservation rows through the repository
Api\SerialNumberDetailsRepositoryInterfaceSerial numbers. getList() treats a filter group as OR, not AND, so filter on the collection if you need AND.
Api\FixedRentalDatesRepositoryInterface, Api\FixedRentalNamesRepositoryInterfaceFixed rental date sets
Api\ShipmentRepositoryInterface, Api\ReturnsRepositoryInterfaceSend and return records
Api\SendReturnApiProcessorInterfaceShip or return serials. It is also exposed over REST as POST /V1/salesigniter_rental/sendserials and /returnserials, the only REST routes; everything else is GraphQL. It returns a {status_code, status_message} envelope instead of throwing, so check the envelope.

For an order’s dates, Helper\OrderDates::getDatesForOrder($order) and getRentalDatesForOrderItem($orderId, $productId) read them back from the order.

use SalesIgniter\Rental\Api\StockManagementInterface;

class FreeUnits
{
    public function __construct(private StockManagementInterface $stock) {}

    public function forDates($product, string $from, string $to): int
    {
        // $from / $to in Y-m-d H:i:s, store wall-clock
        return (int) $this->stock->getAvailableQuantity($product, $from, $to);
    }
}

Hooking pricing #

Every rental price comes from Model\Product\PriceCalculations. The storefront quote (salesigniter_rental/ajax/price), the cart and admin order create call calculatePrice($productId, $fromDate, $toDate, $qty, ...), usually by way of Model\Product\Price::getBasePrice(). calculatePrice() calls calculatePriceBreakdown() and returns its price. The GraphQL pricing query calls calculatePriceBreakdown() directly. So an after-plugin on calculatePriceBreakdown() is the one place that changes the price everywhere. It returns price, from_date, to_date, qty, has_special_pricing and chunks. If you change price, change chunks as well, so the GraphQL breakdown still adds up.

<!-- etc/di.xml -->
<type name="SalesIgniter\Rental\Model\Product\PriceCalculations">
    <plugin name="acme_rental_weekend_surcharge" type="Acme\Rental\Plugin\WeekendSurcharge"/>
</type>
namespace Acme\Rental\Plugin;

class WeekendSurcharge
{
    public function afterCalculatePriceBreakdown($subject, array $result, $productId, $fromDate, $toDate, $qty)
    {
        // $result['price'] is the rental price for $qty units over the period
        return $result;
    }
}

A bundle priced per product has no price grid of its own: Plugin\Bundle\Model\Product\Price sums the children’s rental prices. Configurables go through Plugin\Magento\ConfigurableProduct\...\Configurable\Price. The storefront never shows a total of zero or less, which keeps a bundle with nothing selected from printing $0.00.

Hooking availability #

To take stock off sale, write a reservation row rather than intercepting the answer. Every reader, including the calendar, cart validation, reports, GraphQL, MSI and the add-ons, then agrees without any further work. Maintenance tickets and iCal imports both work this way. Use StockManagementInterface::addOrUpdateReservationsOrdersTableRowFromArray() with order_id 0 and an existing order_type such as manual. The inventory report counts an unrecognised type as a customer order.

If you write rows with your own SQL, call Model\InventoryUpdatedObservers::fireObservers($productId) afterwards. It clears the per-request availability memo and purges the product’s sirent_avail_<id> page-cache tag. Without it, cached product pages go on showing the old calendar.

A plugin on checkIntervalValid() or getAvailableQuantity() changes only what the server accepts. The browser calendar builds its own answer from an embedded payload, so it would go on offering the dates. If you must change the rule itself, change both implementations described above.

Events the extension observes #

In 1.2.205 the extension does not dispatch events of its own for bookings. It observes Magento’s, and you can observe the same ones after it, or plug into StockManagement.

EventObserverWhat it does
checkout_cart_product_add_before, checkout_cart_product_add_afterPreventMixedCheckout*, UpdateGlobalDates, UpdateBundleItemPriceMixed-cart rules, global dates, bundle prices
sales_quote_item_qty_set_after, sales_model_service_quote_submit_beforeQuantityValidatorObserverRefuses dates or quantities that are not available
catalog_product_type_prepare_full_optionsPrepareOptionsObserverCopies the dates from the buy request into the Start and End Date options
sales_order_invoice_payInvoicePayReserves in “When Invoiced” mode
sales_order_item_cancel, sales_order_creditmemo_save_afterCancelOrderItemObserver, RefundOrderInventoryObserverRelease reserved stock
sales_order_shipment_save_commit_afterShipmentSaveCommittedSend and return bookkeeping
catalog_product_delete_after_done, sales_order_delete_commit_afterClean-up observersRemove reservation rows
salesrule_rule_condition_combineRentalConditionRulesAdds rental conditions to cart price rules
sales_convert_order_item_to_quote_item (admin)OrderItemToQuoteItemKeeps dates when an order is edited or reordered

Plugins and preferences worth knowing #

A preference replaces a class outright. If another module prefers the same class, only one of them wins, and that is the most common kind of conflict. These are ours:

ReplacedBy
Magento\Sales\Api\OrderItemRepositoryInterfaceModel\Order\ItemRepository
Magento\Bundle\Pricing\Price\BundleSelectionPricePlugin\Magento\Bundle\Pricing\Price\BundleSelectionPrice
Magento\CatalogInventory\Observer\RevertQuoteInventoryObserver and the InventorySales oneRental-aware versions under Observer\CatalogInventory\
Magento\Catalog\Block\Product\ListProduct (frontend)Block\Product\ListProduct

The plugins most likely to meet yours:

  • Stock checks. Plugins on StockRegistryInterface, StockStateProvider and, on MSI, IsProductSalableForRequestedQtyConditionChain and GetStockItemConfiguration let rental products skip Magento’s salable-quantity checks. Rental stock is dates, not a counter.
  • Order placement. OrderManagementInterface::place() is wrapped. When a rental order has no shipping, it gets a copy of the billing address and free_shipping, so an active Free Shipping method must exist. The order resource model’s afterSave() writes the reservation rows.
  • Product save. ProductRepositoryInterface and Catalog\Model\Product plugins create the rental custom options.
  • Prices on screen. Framework\Pricing\Render and PriceBox plugins render the rental pricing card in place of Magento’s price.
  • Dates on item lines. Plugin\Sales\Item\OptionsDateFormat formats the date options on the storefront, in the admin, in emails and on PDFs.
  • Search. A before-plugin on Magento\Elasticsearch\...\Aggregation\Interval nudges a zero price lower bound to 1.0e-6. Rental products have a zero catalogue price, and core builds an empty range for a zero bound, which makes OpenSearch return a 400 for the whole category page. Leave it in place: it has no effect once Adobe fixes the core bug.
  • Every block. FetchViewModifier wraps Magento\Framework\View\Element\Template::fetchView(). See the next section.

To find out who wins a class or a plugin chain on a given install, use bin/magento dev:di:info "Magento\Sales\Api\OrderItemRepositoryInterface". When another module breaks the calendar or checkout, first disable it on a staging copy to confirm, then compare its preferences and plugins against the lists above. The fix is usually a sortOrder or a small bridge module of your own.

Themes: Luma, Hyvä and the compatibility switches #

The template plugin, and what replaced it #

Older versions rewrote the finished HTML of blocks: a plugin on Template::fetchView() parsed the HTML and changed it. That renamed buttons, hid options and injected the calendar. Since 1.2.197 most of that work is done the ordinary Magento way, with layout XML, ViewModels, narrowly scoped plugins and two copied core templates:

  • Magento_Catalog::product/view/addtocart.phtml, used for rental products under the catalog_product_view_type_sirent, _bundle and _configurable handles. It renders “Rent” and the Buyout button. If your theme overrides addtocart.phtml, compare it with ours. For non-rental products the template renders core’s markup unchanged.
  • Magento_Shipping::create/items/renderer/default.phtml, only under adminhtml_order_shipment_new, for the serial pickers.

Rental product pages get the body class sirent-rental-product. The pricing card carries data-role="sirent-pricing-card", and the stylesheet keys on that attribute to hide Magento’s own price displays. A custom card that drops it shows two prices.

Each replaced behaviour has a switch that puts the old HTML-rewriting path back for a customised theme. They are under Stores > Configuration > Sales Igniter > Rental > Theme Compatibility (salesigniter_rental/legacy_template_plugin/*), and all default to No. Turning one on also makes the new code stand down, so the two never both run.

SwitchTurn it on when
rent_buttonA rental shows “Add to Cart” instead of “Rent”, or the Buyout button is missing, on the product page or in listings
zero_priceRental product pages show a $0.00 price
rental_optionsThe Start Date, End Date, Rental Buyout or Damage Waiver options are visible, or the calendar is missing
item_datesAdmin item tables repeat the Start Date and End Date rows, or hide the wrong ones
serial_inputsThe serial pickers are missing on Sales > Shipments > New Shipment
admin_order_jsThe rental calendar stops working on Create New Order

The plugin itself stays registered. It still renames the listing buttons (now only on ListProduct and ProductsList), places the admin order calendar, and adds dates to emails and the admin order view. Its per-block cost is gone.

Hyvä #

On a Hyvä store, install salesigniter/hyvarental. It needs hyva-themes/magento2-compat-module-fallback, and supplies the add-to-cart button, the pricing card and the out-of-stock modal in Alpine, with no RequireJS. Its layout files use the hyva_ handle prefix, so they apply to Hyvä themes only, and a Luma store is unaffected. A Luma store should not install it.

  • Helper\Data::isHyvaTheme() asks Hyvä’s own theme service, so a child of Hyva/default is recognised. Force Hyva Theme Mode (salesigniter_rental/theme/is_hyva) forces it on, for headless or heavily customised front ends. There is no way to force it off.
  • The Previous Date Picker needs jQuery UI and RequireJS, so a Hyvä store always gets the current calendar.
  • Layout before= and after= only work between siblings, and the block tree differs between Hyvä and Luma. If a block lands in an odd place on your theme, check that its anchor really is a sibling there. Layout never warns about this.
  • The calendar input has id="flatpickr", so window.flatpickr can be that element. Test for the library with typeof window.flatpickr === "function".

The date pickers and their JavaScript #

Calendar Options > Date Picker (salesigniter_rental/calendar_options/picker) has four values: inline (the default), popup, separate and previous. The first three are one flatpickr-based engine, view/base/web/js/flatpickr/sibookingFlatpickr.js, with the rules in sibookingCalendarRules.js and the time slots in the RequireJS module sibookingSlotGrid (window.SibookingTimepicker points at it). previous is the jQuery UI pprdatepicker.js that shipped before 1.2.197, kept so it can be used to rule the new picker out; the admin order screens still use it. The old sitimepicker dropdown is gone, so code that required sibookingTimepicker should require sibookingSlotGrid.

The calendar and the full page cache #

The product page embeds the product’s availability (Model\Calendar\AvailabilityPayload) instead of fetching it on every load, so the page cache must be told when a booking changes it. The calendar block reports the identity sirent_avail_<productId>, and every reservation write purges it through InventoryUpdatedObservers. That works with the built-in cache and with Varnish. Don’t mark the calendar block cacheable="false": one uncacheable block makes the whole page uncacheable.

A related trap: Advanced > Record out of stock rentals (salesigniter_rental/advanced/record_outofstock) adds an uncacheable block to every page. With it on, the full page cache stores nothing, site-wide. Check it before you measure cache behaviour.

GraphQL API #

The rental API is GraphQL, on Magento’s own /graphql endpoint, with nothing to enable. It is documented in its own section:

When you extend the schema, these points matter:

  • The schema is in each module’s etc/schema.graphqls. Resolvers live in Model/Resolver/, and shared services in Model/GraphQl/ (Authorization, ProductLocator, DateInput, Pagination, PriceFormatter and others). Reuse them rather than writing parallel ones.
  • Back-office fields are authorised with the existing ACL: an admin or integration bearer token that can reach /rest/V1 can reach them. Storefront fields are not gated.
  • etc/graphql/di.xml registers a type resolver for sirent. Without it, the standard products query returns null for every rental product and reports “Concrete type for ProductInterface not implemented”.
  • The schema stitcher reads comments. Magento finds types in .graphqls files with a regular expression over the raw text, and it doesn’t skip # comments. A comment such as “This type adds that” defines a type called adds, the real type disappears, and the whole endpoint fails with Syntax Error: Unexpected Name "that", naming no file. Never write type, interface, union, enum, input or scalar followed by a word in a comment. Document fields in """ docstrings or @doc(description:) instead. php vendor/salesigniter/releaserental2/Test/E2e/schema-lint.php runs the same regular expression and names the offending line.
  • After adding a type resolver or other GraphQL DI, cache:flush may not be enough. Clear generated/metadata as well.

Cron jobs and CLI commands #

Magento cron must run, ideally every minute. None of these jobs is needed to reserve stock at checkout, but reminders, iCal imports, the Utilization report and the date migration all wait on it.

JobGroupRunsDoes
salesigniter_rental_send_reminderssirent_remindersevery 5 minQueues and sends reminder emails. It is safe to run late or twice, because reminders are de-duplicated in sirental_reminders.
async_inventory_updatesirent_remindersevery 15 minThe reservation reconciler (see above)
salesigniter_rental_migrate_datessirent_remindersevery 5 minRuns the date migration while it is pending, otherwise nothing
update_utilizationsirent_utilizationevery minuteBuilds Utilization reports that have been requested
salesigniter_rental_sync_ical_importssirent_icalevery 10 minSyncs subscribed external calendars, salesigniter_rental/ical/import_batch_size at a time
CommandModuleDoes
salesigniter:rental:migrate-datesreleaserental2Date storage migration (--dry-run, --orders-only, --quotes-only, --item-id, --limit)
salesigniter:Remove:Attributes, salesigniter:Restore:Attributesreleasecommon2Detach the rental attribute source models before disabling the extension, and put them back after re-enabling
salesigniter:Uninstallreleasecommon2Deletes the rental attributes, tables and reservation history. Read Uninstall or Disable first.

Tests #

The tests ship with the package under vendor/salesigniter/releaserental2/Test/. Run them from the Magento root. Judge a PHPUnit run by its OK or FAILURES line, not by the exit status: a missing Allure config file adds a runner warning that sets the exit status to 1 on its own.

# unit - no database
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist vendor/salesigniter/releaserental2/Test/Unit

# integration against your DEVELOPMENT database - creates and deletes its own products and orders
vendor/bin/phpunit -c vendor/salesigniter/releaserental2/Test/Integration/phpunit.xml

# calendar rules in node - one file at a time; "node --test Test/Js/" fails before running anything
node --test vendor/salesigniter/releaserental2/Test/Js/rules.test.js

Never point the integration suite at a production database. It writes real products, orders and reservations, and cleans up after itself only when it finishes. Some of its tests use one hand-maintained product, whose id is SI_TEST_PRODUCT_ID in its phpunit.xml. Point that at a date-only rental product of your own.

The schema tests in Test/Integration/Schema need a clean install, so they run under Magento’s own integration framework instead:

rm -rf generated/metadata        # not optional, see below
cd dev/tests/integration
php -d memory_limit=-1 ../../../vendor/bin/phpunit -c phpunit.xml.dist \
    ../../../vendor/salesigniter/releaserental2/Test/Integration/Schema

Browser tests (Playwright) and a GraphQL end-to-end check are in Test/E2e/; Test/E2e/README.md explains the setup. graphql-smoke.sh proves the schema stitches, every resolver can be constructed and the ACL refuses what it should.

The traps, each of which has cost somebody a day:

  • generated/metadata overrides every di.xml. When compiled DI exists, Magento reads nothing else, and the integration framework uses the dev install’s compiled DI. A DI change then appears to do nothing, with no message. Delete generated/metadata before an integration run and after any DI change.
  • Stale generated code looks like a missing class or a bad argument. After switching branches or pulling a constructor change, errors like Class ... not found or “Argument #12 must be of type …, array given” usually mean generated/ is stale. Run rm -rf generated/code/* generated/metadata/*, then setup:di:compile.
  • Adminhtml plugins don’t fire in the harness. It boots the frontend area, so a test of an etc/adminhtml/di.xml plugin has to apply the plugin by hand.
  • Place test orders on a store view, not store 0. MSI resolves stock from the website, and the admin website has none.
  • A test that runs DDL needs @magentoDbIsolation disabled on the class docblock. Otherwise the first DROP TABLE waits forever on the framework’s own open transaction. Kill the leftover MariaDB threads before the next run.

Debugging #

  • Why is a date blocked? Query the rows: SELECT reservationorder_id, order_id, order_type, qty_use_grid, start_date_use_grid, end_date_use_grid, source_code, history FROM sirental_reservationorders WHERE product_id = 123 AND end_date_use_grid > NOW() ORDER BY start_date_use_grid;. A row with order_id = 0 is a manual, maintenance or iCal block. comments starting ical: names the import.
  • What does the calendar think? The product page embeds the availability payload. A popup picker or an admin screen calls salesigniter_rental/ajax/booked. Prices come from salesigniter_rental/ajax/price. Watch the browser’s network tab and compare the answer with the rows.
  • The calendar is right after a refresh but wrong from cache: something wrote reservation rows without calling InventoryUpdatedObservers::fireObservers().
  • Orders reserved late: search var/log/system.log for “reconciliation cron reserved inventory”. The ERROR logged at placement just before it gives the cause.
  • A new plugin or preference has no effect: cache:flush, then setup:di:compile in production mode. cache:clean config alone has been seen not to be enough.
  • “Please upgrade your database” after deploying: setup:upgrade didn’t finish. Run it again and read its output. Don’t put helper classes in Setup/Patch/Data/: Magento registers every PHP file there as a patch.
  • Luma looks broken after an upgrade but the logs are clean: stale static files. See the next section.

Upgrading to Magento 2.4.9 #

2.4.9 runs on PHP 8.3 or 8.4. None of the steps below announces itself when it goes wrong:

  1. Raise the rental modules in the same Composer run as Magento. 2.4.9 brings Symfony Console 7, where Command::execute() must return int. Older releasecommon2 and releaserentalinventory2 commands don’t, and Magento loads every command, so every bin/magento call dies, including setup:upgrade. Require releasecommon2 1.2.54 or later and, on MSI, releaserentalinventory2 1.2.47 or later. Both work on 2.4.8 too. Third-party modules with the same problem (Xtento’s exporters, for example) must be updated or disabled.
  2. Re-apply security patches. Composer replaces the patched vendor files. Check Adobe’s bulletins for fixes shipped as separate hotfixes for 2.4.9, such as APSB26-146, and apply them before leaving maintenance mode.
  3. Wipe the static files instead of redeploying over them. setup:static-content:deploy -f skips files that already exist, so the new markup gets the old CSS. On Luma the top navigation collapses to zero height while HTTP, the logs and the file count all look fine. A partial wipe can leave RequireJS unable to resolve module names. Always do the full wipe below.
  4. Reindex after setup:upgrade: it invalidates indexers without rebuilding them.
  5. Redis: 2.4.9 moved the cache to Symfony Cache. backend in env.php must be the literal string redis. A unix-socket connection is built into an invalid DSN, and Magento quietly falls back to the file cache. If var/cache fills up while Redis stays empty, use a TCP host and port or patch SymfonyAdapterProvider.
bin/magento maintenance:enable
composer require magento/product-community-edition:2.4.9 --no-update
composer require "salesigniter/releasecommon2:>=1.2.54" --no-update
composer require "salesigniter/releaserentalinventory2:>=1.2.47" --no-update   # MSI only
composer update
bin/magento setup:upgrade
bin/magento setup:di:compile                     # production mode
rm -rf pub/static/frontend pub/static/adminhtml pub/static/deployed_version.txt
rm -rf var/view_preprocessed/* var/cache/* var/page_cache/*
bin/magento setup:static-content:deploy -f en_US  # add your locales
bin/magento indexer:reindex
bin/magento cache:flush
bin/magento maintenance:disable

Then check the storefront in a real browser. A 200 response and a clean log proved nothing on the installs where the static-file problem appeared. PHP 8.4 raises deprecation notices, not errors, for older contract, maintenance and extension add-on versions; update them anyway.

Conventions in this codebase #

  • Schema is declarative. etc/db_schema.xml plus db_schema_whitelist.json; the old InstallSchema and UpgradeSchema are gone. Name composite indexes with an explicit referenceId, or every setup:upgrade churns them.
  • Setup/Patch/Data/ holds patch classes only, each named after its file. Shared code lives one directory up.
  • Dates are MySQL wall-clock strings everywhere (see above). Format them only at the edge.
  • Rules exist twice, in PHP and in JavaScript, held together by the shared fixtures in Test/fixtures/.
  • Reservation writes announce themselves through InventoryUpdatedObservers. A unit test, ReservationWritePathsTest, fails when a file writes rows without doing so.
  • A refusal is recorded, never silent. Reminders that are not sent get a row with a reason. The iCal import never frees dates because a fetch failed.
  • GraphQL fields are documented in docstrings, never in # comments.
  • Versions: composer.json version and etc/module.xml setup_version always match, and tags are the bare version, for example 1.2.205.