The format is based on Keep a Changelog and this project adheres to Semantic Versioning. Instead of change type headers, we use module names.
All unreleased changes to this project will be documented in this file. Older changelogs can be found in folder changelogs.
window.crmAdmin.initClipboard) bundling ClipboardJS directly into admin.js. remp/crm#3856
.clip class now gets copy-to-clipboard behavior and a translated “copied” tooltip automatically, with no per-page script needed.AdminLoggedAuthorization to authorize API calls only by the session of a logged-in admin; a user access token sent as a bearer token is no longer accepted. remp/crm#3898
SecuredAdminAccess, e.g. SSO login when admin_secure_login_check is enabled).InternalToken. Admin pages should call APIs authorized by AdminLoggedAuthorization (or AdminLoggedOrBearerTokenAuthorization), external clients should use their own API token. remp/crm#3898api_logs_* archive table being left behind on new installations. remp/crm#3830AdminLoggedOrBearerTokenAuthorization, which accepts either the session of a logged-in admin (calls from CRM admin pages) or an API token (server-to-server calls). remp/crm#3898TokenAuthorizationInterface (getAuthorizedToken()), through which an API authorization provides the token that authorized the request. ApiPresenter uses it to record the token in API logs and stats. remp/crm#3898TokenStatsTrackableInterface; API token stats are now updated only for calls authorized by an authorization implementing it. remp/crm#3898apple_appstore_subscription_types table, so create, update and delete operations are recorded in the audit log. remp/crm#3725offer_discount_type and offer_period columns to the apple_appstore_subscription_types table.subscription_types.next_subscription_type_id: the existing pairing is copied into a FREE_TRIAL offer row and the base row is re-pointed to next_subscription_type_id. The rollback restores the schema only, not the re-pointed base rows.OfferTypeEnum and OfferDiscountTypeEnum covering the values of Apple’s offerType and offerDiscountType fields, so seeders and other CRM code do not have to reach for the constants of the readdle/app-store-server-api library.appleAppstoreSubscriptionTypeResolver service (SubscriptionTypeResolverInterface, SubscriptionTypeResolver) with resolve() for the product and the offer of Apple’s TransactionInfo and resolveForRenewal() for the upcoming renewal.VerifyPurchaseV2ApiHandler matching an already created payment by subscription type as well as by transaction ID. With more than one subscription type per product this could create a second payment.AppleAppstoreSubscriptionTypesRepository::findSubscriptionTypeByAppleAppstoreProductId(), use findSubscriptionType() or findOfferSubscriptionType() instead. It is kept only for the legacy Apple App Store API v1 receipt handling and will be removed in the next major release.ServerToServerNotificationV2Processor::getSubscriptionType(), now a thin delegate to SubscriptionTypeResolverInterface::resolve(). Instances overriding it have to override the appleAppstoreSubscriptionTypeResolver service instead. It will be removed in the next major release.appleAppstoreUserResolver service (UserResolverInterface, UserResolver) with resolve() matching the user of Apple’s TransactionInfo. The matching steps (appAccountToken, original transaction ID, unclaimed user creation) are separate protected methods, so an instance can replace just one of them.ServerToServerNotificationV2Processor::getUser(), now a thin delegate to UserResolverInterface::resolve(). Instances overriding it have to override the appleAppstoreUserResolver service instead. It will be removed in the next major release.
ServerToServerNotificationV2WebhookHandler no longer depends on serverToServerNotificationV2Processor; it uses appleAppstoreSubscriptionTypeResolver and appleAppstoreUserResolver directly.VerifyPurchaseApiHandler, VerifyPurchaseV2ApiHandler) unpairing the device from access tokens of other users logged in on the same device. Now only the purchase owner is paired, and other users logged in on the device stay paired with it. remp/helpdesk#5021VerifyPurchaseApiHandler, VerifyPurchaseV2ApiHandler, both server-to-server notification webhook handlers) to expire after 30 seconds, so a lock left behind by a terminated process no longer blocks the transaction forever. remp/helpdesk#5156
ExecutionOutsideLockException once it completes.phinx:dump-schema-for-tests to build the dump by running migrations of every module present in the project (registered or not) into a temporary database, instead of dumping the currently configured live database. The dump is now reproducible, covers modules of all environments and no longer depends on the state of anyone’s development database. remp/crm#3830
CREATE DATABASE/DROP DATABASE privilege; the command works in a temporary <CRM_DB_NAME>_schema_dump database and drops it afterwards.tests/migrations/all_tables.sql (raw CREATE TABLE statements); tests/migrations/19700101010000_all_tables.php became a small static migration executing that file and is no longer generated.--name, --overwrite, --path, --environment); the command takes no options.odan/phinx-migrations-generator and riimu/kit-phpencoder dependencies as unnecessary; the command no longer uses them. The tests/migrations/schema.php snapshot file is no longer generated.schema:load (bin/tests.php) to drop and recreate the test database before loading the schema. remp/crm#3830
--force skips the prompt. CI test jobs now run schema:load --force.MemoryUserDataStorage, an in-memory UserDataStorageInterface implementation resettable between tests; the test environment now uses it as the user_data storage instead of Redis. remp/crm#3765JE (Jersey), MQ (Martinique), AS (American Samoa), AI (Anguilla) and VI (Virgin Islands (U.S.)) to CountriesSeeder.hermes_tasks_old archive table being left behind on new installations. remp/crm#3830EventsStorage::register() to accept an optional $name parameter, letting modules set a custom human-readable label for a registered event instead of always auto-deriving it from $code. remp/novydenik#1593SensitiveTriggerParamsInterface, an optional interface for scenario trigger handlers to declare output params (e.g. passwords) that must not be stored or logged in cleartext. remp/crm#3371FormPatterns::PHONE_NUMBER_INTERNATIONAL, PHONE_NUMBER_FLEXIBLE) rejecting valid 10-digit numbers, e.g. Norwegian +47 123 45 678. remp/helpdesk#5106events/list and event-generators/list APIs to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call them without the internal API token. API tokens keep working. remp/crm#3898ChurnPredictionModule module. remp/crm#3768
training mode).scoring mode, which exports current subscription data and runs churn predictions on them using already trained model.churn:run_pipeline command (RunChurnPipelineCommand).churn_predictions table (AddChurnPredictionsTable migration) storing the latest churn prediction per subscription in the CRM DB, with foreign keys to users and subscriptions. Accessed via ChurnPredictionsRepository. remp/crm#3768churn:download_churn_predictions command (DownloadChurnPredictionsCommand), which downloads the most recent predictions from BigQuery and upserts them into the churn_predictions table. remp/crm#3768family_subscription_types table). remp/crm#3702
end_time in the future). remp/crm#3806
render() changed to render(?int $userId = null): void; subclasses overriding it have to update their signature.ExtendFamilyExtension chaining a new family subscription after a family subscription stopped before it started, which left the user without access until that date. remp/helpdesk#5154window.crmAdmin.initClipboard helper instead of loading ClipboardJS from a CDN itself. remp/crm#3856created_at and updated_at columns to the google_play_billing_subscription_types table. remp/crm#3725google_play_billing_subscription_types table, so create, update and delete operations are recorded in the audit log. remp/crm#3725VerifyPurchaseApiHandler) unpairing the device from access tokens of other users logged in on the same device. Now only the purchase owner is paired, and other users logged in on the device stay paired with it. remp/helpdesk#5021PaymentInvoiceStatusResolver, replacing three separate implementations that could disagree with each other. remp/crm#3859generate_invoice Hermes message failed once; it is now retried. remp/crm#3634reverseCharge invoice template parameter and the invoices.invoice_template.note.reverse_charge translation, so instance templates can note that the customer accounts for the VAT of a B2B reverse-charge payment. remp/crm#3862InvoiceGenerator) to expire after 30 seconds, so a lock left behind by a terminated process no longer blocks invoice generation for the payment forever. remp/helpdesk#5156
ExecutionOutsideLockException once it completes.assignedAfter filter to NytActivationCodesRepository::userActivationCode() and claimAvailableCode(), restricting the lookup to codes assigned since a given date, and made userActivationCode() return the most recently assigned code. remp/novydenik#1593nyt:import-activated-codes failing to import activation dates after NYT changed their export format. remp/novydenik#1593nyt_activation_code_estimated_end scenario “before event” trigger, firing before the estimated end (of the NYT subscription). remp/novydenik#1593
activated_at (or assigned_at if activation date is missing) + NytActivationCodesRepository::SUBSCRIPTION_LENGTH_DAYS (364 days). We don’t have start time / end time dates for NYT subscriptions (and we don’t have an activation date for all codes).NytActivationCodesRepository#add() to accept also external_id (mandatory column without default value). remp/novydenik#1593OnboardingGoalCompletedCriteria to parse its timeframe with TimeframeScenarioTrait, so the module now requires ScenariosModule. A partially filled timeframe now fails the condition job with an error instead of being ignored, and invalid timeframe values mark the job as failed instead of leaving it in the started state. remp/crm#3650onboarding-goals/list API to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call it without the internal API token. API tokens keep working. remp/crm#3898https://onesignal.com/api/v1/ to https://api.onesignal.com/ and the authorization header from Basic <key> to Key <key>, as required by OneSignal’s App API keys (os_v2_app_...).
onesignal_applications.application_api_key before deploying, unless your legacy key works with the new host and header.api.onesignal.com if you restrict egress traffic.onesignal-templates/list and onesignal-applications/list APIs to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call them without the internal API token. API tokens keep working. remp/crm#3898failed. OneSignal no longer returns recipients in the create-notification response, so a message is now sent when OneSignal returns a notification ID.
recipients column of onesignal_outbound_messages is deprecated and no longer written.no_subscription status of outbound messages, used when OneSignal doesn’t create the notification because no subscription matches the filter (e.g. the user doesn’t use that OneSignal application).failed without being retried. Rate limiting (429), server and connection errors are thrown, so Hermes retries the message.idempotency_key to outbound messages and sent it to OneSignal, so Hermes retries can’t deliver the same notification twice. Already processed messages are skipped by OneSignalSendNotificationHandler.price:update event trigger from admin payment templates (see ProductsModule). remp/crm#3679resumed_subscription_id column to subscription_pauses, a second foreign key to subscriptions (existing pauses are backfilled by the migration). $subscription->related('subscription_pauses') is now ambiguous and throws, use the new SubscriptionPausesRepository::subscriptionPauses() instead. remp/crm#3881
SubscriptionPausesRepository::add() now requires the resumed subscription ID as its fifth parameter.RefundPaymentProcessor::processRefundedPayment() to store the refunded amount in the refund_amount payment meta (RefundPaymentProcessor::REFUND_AMOUNT_META_KEY) instead of each gateway storing its own. remp/helpdesk#5157
processRefundedPayment() after a successful RefundableInterface::refund(). InstantRefundWidget already does.PaypalRefundTrait::REFUND_AMOUNT is removed, read RefundPaymentProcessor::REFUND_AMOUNT_META_KEY instead.PaymentRefundFormFactory to run the PaymentRefundFormDataProviderInterface providers before it changes the payment status, subscription end and recurrent payment, so an instant gateway refund finishes before the status change triggers notifications. remp/helpdesk#5157
RecurrentPaymentTokenMissingEvent, emitted when a payment on a recurrent gateway completes but the gateway doesn’t return a recurrent token (e.g. the bank failed to register the card). remp/crm#3848
recurrent_token_missing payment meta. Replaces the previous “missing recurrent token” error log, which produced unactionable Sentry noise.DonationPaymentItemsListWidget). Used on payment screens (list and show payment). remp/helpdesk#4731ImapClient::fetch() to retry up to 3 times with a fresh connection when the IMAP server drops the connection mid-session (Gmail’s occasional “empty response”), instead of failing the whole mail-confirmation run. remp/crm#3845SubscriptionPauser to own the pausability check by moving isSubscriptionPausable() out of PauseSubscriptionWidget, so the check can be reused outside the widget. remp/crm#3818CopyPaymentService. remp/crm#3252
status index to recurrent_payments table to speed up RecurrentPaymentStatusCriteria scenario criteria load. remp/crm#3371window.crmAdmin.initClipboard helper instead of loading ClipboardJS from a CDN itself. remp/crm#3856RecurrentPaymentChargesDataProviderInterface, letting modules override the failed-charge retry schedule (the recurrent_payment_charges configuration option) per recurrent payment, e.g. to use a different schedule for each payment gateway. remp/crm#3841
RecurrentPaymentsProcessor::processFailedRecurrent() uses the intervals returned by the first provider that returns a non-empty list; otherwise it falls back to the configuration option.PaymentGatewayFormDataProviderInterface, letting modules add their own fields to the admin payment gateway form and process them on submit. remp/crm#3841
PaymentGatewayFormFactory after the form defaults are set, and on submit before the payment gateway is updated, so a provider can adjust or strip the submitted values.PaymentMethodsRepository::findPaymentMethodByRecurrentPayment(), returning the payment method a payment was made with, no matter whether the payment is the parent of a recurrent payment, its charge, or a card change. remp/crm#3871PaymentCopiedEvent, emitted by PaymentsRepository::copyPayment() with the source and the new payment, so modules can carry their own payment-bound data over to the copy (only payment items are copied by default). remp/helpdesk#5013RecurrentPresenter. remp/crm#3897SubscriptionPauser::stopResumedSubscriptions(), called by the new PausedSubscriptionRefundHandler on PaymentChangeStatusEvent.id of the resumed subscription to new_subscription in the result of SubscriptionPauser::pauseSubscription().expired_by_admin subscription meta set, the same as a subscription stopped by the refund form.RecurrentPaymentsResolver::resolveAddress() to return null when the parent payment’s address is deleted, so a renewal no longer binds a deleted address to the new payment and subscription. remp/helpdesk#5055PaymentsRepository::updateStatus() and RecurrentPaymentsRepository::createFromPayment() to expire after 30 seconds, so a lock left behind by a terminated process no longer blocks the payment forever. remp/helpdesk#5156
ExecutionOutsideLockException once it completes.ChangeAddressRequestFormFactory to save the submitted country if a data provider enables the country select. remp/crm#3868
UserPrintAddressFormFactory using the payment’s address only when its type is print; it now compares with the address type the form was created for, so the magazine and reporter forms no longer prefill a bound print address. remp/helpdesk#5055PaymentFormDataProvider no longer creates product or postal fee payment items.CartTrait signal methods and the onSave, onLogin and onAuth callbacks of CheckoutFormFactory. remp/crm#3901
CartTrait that override handleAddCart, handleRemoveCart or handleRemoveProductCart must declare the : void return type.CartTrait must not declare its own $cartSession, $cartProducts or $cartProductSum; the trait now types them.Closure properties; assign a closure.cart form value. The payment is now built from the cart stored in the session; a submission whose cart no longer matches it (for example a checkout page left open in another tab) is rejected with a message and has to be confirmed again. remp/crm#3901
CheckoutFormDataProviderInterface::formSucceeded() receives the session cart as $params['cart']; data providers should read the cart from there instead of $values['cart'].remp/list-banners API to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call it without the internal API token. API tokens keep working. remp/crm#3898MailUserSubscriptionsRepository::subscribeUserAll() and unsubscribeUserAll() to operate only on publicly-listed mail types; previously they affected all mail types regardless of visibility, including hidden ones. remp/helpdesk#4726MailReceivedCriteria scenario condition to check the whole delivery history when its timeframe is left empty; previously it never matched. A partially filled timeframe now fails the condition job with an error instead of being ignored. remp/crm#3650MailTypeSubscriptionCriteria for scenarios to check whether user is subscribed to the newsletter or not. remp/crm#3829hard_bounced_at value. remp/remp#1481mail_type and mail_template.public_preview_url in the users/logs API); with older Mailer versions the columns stay empty.MailTypesAdminPresenter redirecting to the mail type detail in Mailer.MailTemplatesAdmin and MailSettings presenters failing with “not found in DI container” since the module stopped using Composer classmap autoloading. The presenters are now registered in the module’s config. remp/crm#3904login_type_disabled_for_admin login attempt when an admin account tries to log in with a Mailer autologin token. remp/crm#3869mail-template/list API to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call it without the internal API token. API tokens keep working. remp/crm#3898body parameter of SalesFunnelFrontendPresenter::renderShow(); the rendered body is now always the stored sales funnel body. remp/crm#3176
funnel fallback of renderShow() was removed together with it; the funnel url key is now read only from the URL.SalesFunnelRenderer service with the funnel Twig rendering extracted from SalesFunnelFrontendPresenter. remp/crm#3176preview parameter) to no longer count into show statistics. remp/crm#3176
SalesFunnelEvent is not emitted at all for such previews, so sales_funnel scenario triggers also stop firing for them.limit_per_user_for_period to sales funnels, limiting the per-user purchase count only within the last N days before the purchase; when empty, the limit keeps applying to all payments ever made. remp/crm#3742
limit_per_user in the funnel form and offers configurable day presets (see README).SalesFunnelsRepository::add() and getAllUserSalesFunnelPurchases() accept new optional arguments ($limitPerUserForPeriod, $paidFrom).Added custom access-denial destinations through SalesFunnelAccessDeniedException, which salesfunnel.dataprovider.validate_funnel providers can throw instead of redirecting directly. The presenter respects allow_redirect and records the no-access event. remp/crm#3867
admin.sales_funnel.show.middle widget slot to the sales funnel detail page, rendered below the subscription types and payment gateways row. remp/crm#3828SalesFunnelAccessValidator service with the sales funnel access checks extracted from SalesFunnelFrontendPresenter, so other code can check whether a user can access a funnel without being redirected. remp/crm#3872
evaluate() follows redirect funnels and returns SalesFunnelAccessResult with the reason of a denial.SalesFunnelFrontendPresenter now uses the same checks; its redirects and no-access statistics are unchanged.TimeframeScenarioTrait::getTimeframe() to throw ConditionCheckException when only some of the timeframe fields (operator, amount, unit) are filled; previously it returned null, the same as for an empty timeframe. remp/crm#3650scenarios/* APIs (info, create, criteria, generics, stats, list-triggers, users, journeys, journey-jobs) to accept only the session of a logged-in admin (AdminLoggedAuthorization). remp/crm#3898
scenarios_job_history table, preserving its final state. remp/crm#3371
JobHistoryRepository, which records the snapshot from JobsRepository::softDelete().scenarios_job_history table partitioned by month of created_at, so retention can be enforced with cheap partition truncation.scenarios:remove_old_job_history command, which truncates the partitions whose month is outside the retention window.started_at timestamp. remp/crm#3371
embed action of ScenariosAdminPresenter together with embed.latte.journey_id and parent_job_id columns to both scenarios_jobs and scenarios_job_history.repeating value to the scenarios_triggers.type enum. The segment code is stored in event_code and the schedule in options as {"frequency":"weekly","day":4,"time":"09:00"}; the segment has to be based on the users table.scenarios_repeating_trigger_schedule table and RepeatingTriggerScheduleRepository. A unique index on (trigger_id, occurred_at) claims each occurrence before any job is created, so overlapping runs of the generator cannot enroll a segment twice.RepeatingTriggerGenerator and the scenarios:repeating_trigger_generator command (recommended cron interval 10 minutes), with --now, --scan-minutes (how long after its scheduled time an occurrence may still be fired, default 20) and --catch-up-minutes (how far back a due occurrence is still looked up, default 60) options. An occurrence found within the catch-up window but older than the scan window is recorded as skipped and never replayed.user_id to their descendants, so conditions requiring an event entity (subscription, payment, recurrent payment, address, order, and the has_payment trigger criterion) are rejected when the scenario is saved.scenarios_versions table. Every save records the whole definition of the scenario.scenario_version_id to scenarios_jobs and scenarios_job_history, recording which version of the scenario each job ran on.ScenariosRepository::createOrUpdate() to accept the ID of the user saving the scenario. remp/crm#3371/v1/scenarios/users (users who passed through the scenario), /v1/scenarios/journeys (journeys of a user) and /v1/scenarios/journey-jobs.(scenario_id, user_id, journey_id) index to the scenarios_job_history table.subscription-upgraded Hermes message; outputs user_id, subscription_id (the new, upgraded subscription) and upgrade_type.ValidatableSegmentInterface and ValidatableQueryInterface; SegmentQuery now validates that a segment’s query contains the %where% placeholder. remp/crm#3809SELECT %fields% FROM %table% WHERE %where% GROUP BY %group_by%) pre-filled into the SQL field when creating a new segment. remp/crm#3809segments/list API to accept also the session of a logged-in admin (AdminLoggedOrBearerTokenAuthorization), so admin pages can call it without the internal API token. API tokens keep working. remp/crm#3898stripe gateway to no longer store the customer’s payment method for future use. setup_future_usage is now omitted from the payment intent instead of being sent as on_session. remp/euobserver#272
enable_future_usage: true extension option to restore the previous behavior.stripe_recurrent still requests off_session, as charging a stored payment method later is its purpose.enable_future_usage extension option (default false) controlling whether the one-time stripe gateway stores the customer’s payment method (setup_future_usage: on_session). remp/euobserver#272stripe gateway to offer every payment method enabled in the Stripe Dashboard (Bancontact, iDEAL, SEPA Direct Debit, …) instead of card only. payment_method_types is now omitted from the checkout session instead of being sent as [card]. remp/euobserver#272
payment_method_types: [card] extension option to restore the previous behavior.stripe_recurrent still offers card only, as it charges the stored card later.RefundTrait::REFUND_AMOUNT and the Stripe gateways’ own refund_amount payment meta write. The amount is now stored by RefundPaymentProcessor (PaymentsModule) under RefundPaymentProcessor::REFUND_AMOUNT_META_KEY. remp/helpdesk#5157payment_method_types extension option listing the payment method types offered on the one-time stripe gateway’s checkout page. Unset by default, which offers everything enabled in the Stripe Dashboard. remp/euobserver#272description, so the Stripe Dashboard payment row names what was bought. remp/euobserver#272AbstractStripe to be an actual abstract class and removed its begin(), which duplicated the Stripe gateway’s flow and was never reachable — the gateway factory binds every gateway code to a concrete subclass. remp/euobserver#272StripeService::createSubscriptionCheckoutSession(), applying a subscription type’s configured coupon (SubscriptionTypeMeta::COUPON_ID) to the checkout session. remp/euobserver#247InvoicePaidWebhookHandler) to match the applied coupon when resolving the subscription type. remp/euobserver#247checkout.session.async_payment_succeeded and checkout.session.async_payment_failed webhook events, confirming or failing payments made with a delayed notification payment method once Stripe reports the bank’s decision. remp/euobserver#271SubscriptionTypesFormFactory. remp/crm#3828
subscription_type_id is set into $values before the form data providers run, so providers resolve the subscription type by ID on both create and edit.subscription_type_id to detect editing now also run on create — e.g. Mobiletech’s short_name is now saved also when creating a subscription type; previously its value was silently dropped on create.recurrent_charge_before was set to a negative value (charge after expiration). remp/helpdesk#4769SubscriptionGapsFilterInterface and SubscriptionGapsRepository::setSubscriptionsFilter(), limiting which subscriptions cover the user when subscription gaps are calculated. remp/crm#3875SubscriptionGapsRepository. remp/crm#3875SubscriptionsRepository::futureEndingUserSubscriptions() and futureEndingUserSubscriptionsWithContentAccess(), selections of a user’s subscriptions that still grant access now or later, excluding subscriptions stopped before they started (end_time equal to start_time). remp/helpdesk#5154ExtendSameTypeExtension and ExtendSameContentAccess chaining a new subscription after a subscription stopped before it started, which left the user without access until that date. remp/helpdesk#5154SubscriptionTypeUpgradeSchemasRepository; update any raw queries against subscription_type_upgrade_schemas. remp/crm#3828
deleted_at column): remove() sets deleted_at, schema lookups exclude soft-deleted rows.add() does not revive a soft-deleted assignment (seeder reruns don’t re-add assignments removed in admin) and now declares an ActiveRow return type.UpgradeSchemaAssigner model handling assignment and unassignment.trial_early_converted subscription meta without running the upgrade when the user’s active subscription already includes all content access of the trial.ExecutionOutsideLockException once it completes.SubscriptionUpgradedEvent, emitted when an upgrade is completed and recorded in subscription_upgrades. remp/novydenik#1637
SubscriptionUpgradesRepository::add(), so it covers all upgrade types; carries the base subscription, the upgraded subscription and the upgrade type.subscription_upgraded and forwarded as the subscription-upgraded Hermes message, which drives the new ScenariosModule trigger.AccountDeleteWidget, rendered in the frontend.user.settings.account_delete widget placeholder. remp/euobserver#253UserCreatedAtCriteria scenario condition to apply no time restriction when its timeframe is left empty, so it matches every user; previously it never matched. A partially filled timeframe now fails the condition job with an error instead of being ignored. remp/crm#3650UserGroupsRepository::removeFromGroup() now sets deleted_at instead of hard-deleting the row. remp/crm#3774
created_at.UNIQUE(user_id, group_id) index on user_groups to prevent duplicate active rows under concurrent addToGroup() calls; migration dedupes any pre-existing duplicates by keeping the highest-id row per pair.deleted_at instead of hard-deleting the row. remp/crm#3774
GroupsRepository::all(), exists() and findByName() exclude soft-deleted groups.UserGroupsUserDataProvider hard-deletes the user’s user_groups rows during anonymization.UserGroupsCriteria scenario criterion not matching the groups selected in the scenario builder. Selecting one or more user groups filtered users by unrelated group IDs, so the criterion let through the wrong users (or nobody at all). remp/crm#3774
ParentUserGroupsCriteria, which delegates to it, was affected the same way and is fixed too.IpRateLimit service so we can override defaults. remp/helpdesk#4835locale index to users table to speed up LocaleCriteria scenario criteria load. remp/crm#3371invalid state / invalid user state warnings. remp/crm#2114users.sso.google.prompt configuration option, passed to Google’s authorization request. The default is select_account - a re-run authorization request renders the account chooser instead of silently redirecting back to the callback. remp/crm#2114login_type_disabled_for_admin login attempt status to LoginAttemptsRepository, recording an attempt to use a login type that is not available for admin accounts. AutoLoginTokenAuthenticator and AccessTokenAuthenticator record it when an admin account tries to log in with a token. remp/euobserver#248 remp/crm#3869UserCreatedAtCriteria scenario condition never matching when its timeframe was left empty. An empty timeframe now applies no time restriction, so the condition matches every user. remp/crm#3650$unpairOtherUsers parameter to AccessTokensRepository::pairWithDeviceToken(). Set it to false to pair an access token with a device token without unpairing the device from other users’ access tokens. The default (true) keeps the existing behaviour. remp/helpdesk#5021AddressesRepository::softDelete() to emit AddressRemovedEvent, so every deletion (including forced ones from frontend pages) unbinds the address from subscriptions via AddressRemovedHandler. UsersAdminPresenter::handleRemoveAddress() no longer emits the event itself. remp/helpdesk#5055UserLoginAttempts). remp/helpdesk#5139
setStatsDays(int $days) setup call on userLoginAttempts service. If it is not set (default state), all login attempts are counted as before.DefaultUserTokenAuthorization, DeviceTokenAuthorization and UserTokenAuthorization to implement TokenAuthorizationInterface, so API logs record the token that authorized the request (newly also for calls authorized by a device token). remp/crm#3898