## Description
This PR adds integration tests for the Quick Lead workflow, including a
complete end-to-end test with full workflow execution.
### Key Changes
1. **Enabled SyncDriver for integration tests** - Jobs are now processed
synchronously in tests
- Modified `create-app.ts` to use `SyncDriver` instead of `BullMQ`
- Added `MessageQueueExplorer` to discover and register workflow job
handlers
- This enables complete workflow execution in integration tests
2. **Added integration tests for Quick Lead workflow**:
- Verify workflow exists and is active
- Verify workflow version has correct structure (MANUAL trigger, FORM
step, CREATE_RECORD steps)
- Test workflow triggering creates workflow run with correct initial
state
- Test stop workflow run on a running workflow
- **Full end-to-end test**: trigger → submit form → verify Company and
Person records created
### Test Coverage
The complete end-to-end test verifies:
- Workflow triggers and is in RUNNING status (waiting on FORM step)
- Form submission with test data succeeds
- Workflow completes successfully with all steps in SUCCESS status
- Company record is created with correct name and domain
- Person record is created with correct name and email
- Records are properly cleaned up after test
### How to Run Tests
```bash
npx nx run twenty-server:test:integration -- --testPathPattern="quick-lead-workflow"
```
Or with database reset:
```bash
npx nx run twenty-server:test:integration:with-db-reset -- --testPathPattern="quick-lead-workflow"
```
Disable Deactivate option for all standard fields
#16706
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Prevents deactivation UI for standard system fields on the field edit
page.
>
> - Introduces `canFieldBeDeactivated` to flag standard fields
(`createdAt`, `createdBy`, `deletedAt`, `updatedAt`)
> - Hides the "Danger zone" (deactivate/activate/delete controls) when
`!isLabelIdentifier && !readonly && !canFieldBeDeactivated`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b309815700. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
This has been fixed on main already 1 hour ago, this PR now only passes
the field application id instead of the object applicationId that could
be different for example when creating a custom field on a standard
object
For the moment not introducing any logic around application integrity
directly and still relying on the isCustom and standardId definition
This will have to be refactored once we deprecate the standardId
## Context
This PR refactors how authentication context is handled in the GraphQL
resolver layer. Previously, the auth context was baked into the schema
builder context at schema creation time. Now, the auth context is
extracted from the request at query execution time, enabling better
schema caching.
## Implementation
- Removed user-specific data from schema cache key: The schema cache key
no longer includes userId and apiKeyId, allowing the same schema to be
shared across all users in a workspace
- Cache key simplified from
${workspaceId}-${userId}-${apiKeyId}-${url}-${cacheVersion} to
${workspaceId}-${url}-${cacheVersion}
- Removed authContext from WorkspaceSchemaBuilderContext: The schema
builder context is now purely about object/field metadata, not about who
is accessing it
- Created createQueryRunnerContext utility: A new helper that combines
the schema builder context with the auth context from the actual request
- Updated all resolver factories: All 15 resolver factories now use
createQueryRunnerContext to build the full context needed by query
runners at execution time
## Benefits
- Improved cache efficiency: GraphQL schemas are now cached per
workspace rather than per user, significantly reducing memory usage and
schema regeneration
- Cleaner separation of concerns: Schema building (metadata-driven) is
now separate from query execution (auth-driven)
- Fresh auth context: Auth context is always retrieved from the current
request, ensuring it reflects the latest state
- Reduced complexity: Simplified the schema creation path by removing
unnecessary auth checks
## Next steps
- Introduce a hash in redis and expose it in the req object so the yoga
patch can access the hash and use it during its own cache key
computation. This way we will be able to invalidate the local cache in
yoga graphql schema generation without restarting pods.
# Introduction
Added a `WorkspaceRelated` and `AllNonWorkspaceRelatedEntity` to
simplify the `FlatEntityFrom` that now do not expect a string literal to
omit and itself builds the related many to one entities foreign key
aggregators
We now have the type grain over relation to syncable or just workspace
related entities
Added a migrations that sets the fk on missing entities
## Next
In upcoming PR we will be able to introduce such below type
```ts
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/remove-suffix.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
export type UniversalFlatEntityFrom<TEntity extends SyncableEntity> = Omit<
TEntity,
| `${ExtractEntityManyToOneEntityRelationProperties<TEntity> & string}Id`
| ExtractEntityRelatedEntityProperties<TEntity>
| 'application'
| 'workspaceId'
| 'applicationId'
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
[P in ExtractEntityManyToOneEntityRelationProperties<TEntity> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifier`]: string;
} & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifiers`]: string[];
};
```
Two challenges with error messages
- always provide a useful/meaningful error message for the end user
instead of the generic one. eg: show "Wrong password" and not "An error
occured"
- avoid technical details unless error regards a technical feature. eg:
show "An error occured" and not "Invalid post-hook payload."; but do
show "Invalid issuer URL." as it occurs while configuring SSO
What this PR does
- Make userFriendlyMessage mandatory for widely used
GraphqlQueryRunnerException and CommonQueryRunnerException, so that
developers are forced to ask themselves what the error message should
be, and as it contains very wide error codes (eg: "Bad request") which
should not be mapped to just one default message
- Keep userFriendlyMessage optional for service-specific exceptions (eg:
workflowStepExecutorException), but convert the error code to
userFriendlyMessage mapper to a switch case function with a typecheck
ensuring that all codes are mapped to a message. These default messages
are still overridable where they are thrown.
Fixes [#16805](https://github.com/twentyhq/twenty/issues/16805)
User was not able to update his own profile picture it showed permisson
error while doing so.
Updated permission so that user is able to edit profile picture
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Ensures profile picture uploads succeed only with appropriate
permissions and clearer errors.
>
> - Tightens `UploadProfilePicturePermissionGuard` to handle missing
`workspace`, allow during workspace creation, and permit uploads when
user has `WORKSPACE_MEMBERS` or `PROFILE_INFORMATION` settings;
otherwise throws a `PermissionsException` with a user-friendly message
> - In `user-workspace.resolver.ts`, validates the upload result and
throws an error if no files were returned
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3766bac15e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
# Introduction
In this pull-request we're refactoring the page layout widget
configuration entity to be containing its discriminated key simplifying
underlying code and maintainability
- Made the configuration and title non nullable
- Introduced a generic predicate for the `widgetConfigurationType`
- Upgraded command to remove `graphType` and insert new
`configurationType` to existing entries
- Migrated frontend to new type system
---------
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Had to close the other PR since I messed up re-basing somehow:
https://github.com/twentyhq/twenty/pull/16668/files.
Moved changes to this new PR in order to resolve comments from the
previous PR and also match the field-design to that on Figma.
---------
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Closes [1957](https://github.com/twentyhq/core-team-issues/issues/1957).
`getWidgetCardVariant` now returns `side-panel` for mobile and right
drawer instead of returning `record-page`.
Added stories to WidgetRenderer.stories.tsx because WidgetRenderer is
where getWidgetCardVariant is called and the variant is applied. These
stories test the actual behavior (mobile/side panel triggering
side-column variant) rather than just visual appearance, following the
existing pattern of individual behavior-testing stories in this file.
Following [discord
thread](https://discord.com/channels/1130383047699738754/1453910755387899996/1453910755387899996),
reproductible on twenty-eng
https://github.com/user-attachments/assets/ae7f363d-87e1-44fa-8fe2-ee78412d62a7
Records positions are computed at record creation, depending on the
position arg from the request, being equal to `last`, `first`, or not
present.
When being equal to last, as it is done when creating a note from the
product, the position is calculated using `.maximum()` function which
uses postgres' MAX function. If there is a `NaN` value among the list,
the MAX will return `NaN` too. So if for some reason there is a NaN
somewhere in the position column, all subsequent records being created
with last position argument will be created with NaN value. Until [this
PR](https://github.com/twentyhq/twenty/pull/16630), where we introduced
a validation on position at record creation which throws when NaN is
trying to be introduced as a position, this was going silent. (fyi
@etiennejouan , not on you at all but for info)
Looking into twenty-eng workspace, I found note records with NaN
position dating back to august 2025, making it hard to understand and
debug why they were introduced with NaN position. So I did not find the
real root cause, but I suggest to
- update record-position.service to fix the issue for subsequent records
that go through this service (which is what is currently broken)
- run a command to fix the existing records with NaN position for Notes,
as it is where the issue happened for both the user reporting the issue
on discord, and us on twenty-eng. So hopefully the problem was limited
to Notes
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Implements last-modifier tracking and unified actor injection.
>
> - New `ActorFromAuthContextService` replaces createdBy-only logic;
pre-query hooks now inject both `createdBy` and `updatedBy` on create
and `updatedBy` on update
> - Add `updatedBy` standard field to core/custom objects (e.g.,
`person`, `company`, `task`, `note`, `attachment`, `dashboard`,
`workflow`, `workflowRun`) with IDs, metadata builders, and ORM entity
fields
> - Record CRUD: `create` now sets both `createdBy` and `updatedBy`;
`update` sets `updatedBy`; workflow actions use a shared workflow actor
builder; REST base handler uses the new actor service
> - Seeder data and snapshots updated to include `updatedBy`; GraphQL
result formatting adds defaults/handling for empty composite fields
(incl. actor)
> - Frontend `useUpdateOneRecord` upserts returned record into the local
store after mutation
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1f283778e9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Bumps
[@opentelemetry/auto-instrumentations-node](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/auto-instrumentations-node)
from 0.60.0 to 0.60.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-js-contrib/releases"><code>@opentelemetry/auto-instrumentations-node</code>'s
releases</a>.</em></p>
<blockquote>
<h2>instrumentation-aws-lambda: v0.60.1</h2>
<h2><a
href="https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-aws-lambda-v0.60.0...instrumentation-aws-lambda-v0.60.1">0.60.1</a>
(2025-11-24)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>devDependencies
<ul>
<li><code>@opentelemetry/propagator-aws-xray</code> bumped from ^2.1.3
to ^2.1.4</li>
<li><code>@opentelemetry/propagator-aws-xray-lambda</code> bumped from
^0.55.3 to ^0.55.4</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/auto-instrumentations-node/CHANGELOG.md"><code>@opentelemetry/auto-instrumentations-node</code>'s
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/open-telemetry/opentelemetry-js-contrib/compare/auto-instrumentations-node-v0.60.0...auto-instrumentations-node-v0.60.1">0.60.1</a>
(2025-06-05)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@opentelemetry/instrumentation-hapi</code> bumped from
^0.48.0 to ^0.49.0</li>
<li><code>@opentelemetry/instrumentation-koa</code> bumped from ^0.50.0
to ^0.50.1</li>
<li><code>@opentelemetry/instrumentation-mongodb</code> bumped from
^0.55.0 to ^0.55.1</li>
<li><code>@opentelemetry/instrumentation-net</code> bumped from ^0.46.0
to ^0.46.1</li>
<li><code>@opentelemetry/instrumentation-redis</code> bumped from
^0.49.0 to ^0.49.1</li>
<li><code>@opentelemetry/instrumentation-restify</code> bumped from
^0.48.0 to ^0.48.1</li>
<li><code>@opentelemetry/instrumentation-undici</code> bumped from
^0.13.0 to ^0.13.1</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/open-telemetry/opentelemetry-js-contrib/commits/auto-instrumentations-node-v0.60.1/packages/auto-instrumentations-node">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary
When users click 'Subscribe Now' during a trial period without a payment
method configured, they previously saw an unhelpful error message: "No
payment method found. Please update your billing details."
This PR improves the UX by redirecting users directly to Stripe's
billing portal payment method update flow, where they can add their
payment information. After adding a payment method, users are redirected
back to the billing settings page and can click 'Subscribe Now' again to
successfully activate their subscription.
## Changes
### Backend
- **stripe-billing-portal.service.ts**: Added
`createBillingPortalSessionForPaymentMethodUpdate` method that creates a
Stripe billing portal session with `flow_data.type:
'payment_method_update'`
- **billing-portal.workspace-service.ts**: Added
`computeBillingPortalSessionURLForPaymentMethodUpdate` method to build
the portal URL
- **billing-end-trial-period.output.ts**: Added optional
`billingPortalUrl` field to the GraphQL output DTO
- **billing-subscription.service.ts**: Modified `endTrialPeriod` to
return `stripeCustomerId` when no payment method exists
- **billing.resolver.ts**: Updated resolver to orchestrate billing
portal URL generation when no payment method
### Frontend
- **useEndSubscriptionTrialPeriod.ts**: Redirect to billing portal URL
instead of showing error snackbar
- **endSubscriptionTrialPeriod.ts**: Added `billingPortalUrl` to
mutation response fields
## User Flow
1. User clicks "Subscribe Now" during trial
2. Backend checks for payment method
3. If no payment method → redirect to Stripe billing portal payment
method update page
4. User adds payment method in Stripe
5. User is redirected back to `/settings/billing`
6. User clicks "Subscribe Now" again to activate subscription
## Summary
This PR implements credit rollover functionality for billing, allowing
unused credits from one billing period to carry over to the next (capped
at the current period's subscription tier cap).
## Changes
### New Services
- **StripeCreditGrantService**: Interacts with Stripe's Billing Credits
API to create credit grants, retrieve customer credit balances, and void
grants
- **BillingCreditRolloverService**: Contains the rollover logic -
calculates unused credits and creates new grants for the next period
- **BillingWebhookCreditGrantService**: Handles
`billing.credit_grant.created` and `billing.credit_grant.updated`
webhooks to update billing alerts
### Modified Services
- **StripeBillingAlertService**: Updated to include credit balance when
calculating usage threshold alerts
- **BillingUsageService**: Returns rollover credits to the frontend for
display
- **BillingSubscriptionService**: Queries credit balance when creating
billing alerts
- **BillingWebhookInvoiceService**: Triggers rollover processing on
`invoice.finalized` webhook
### Frontend
- Updated `SettingsBillingCreditsSection` to display base credits,
rollover credits, and total available
- Updated GraphQL query to fetch new `rolloverCredits` and
`totalGrantedCredits` fields
### Stripe SDK Upgrade
- Upgraded from v17.3.1 to v19.3.1 to get proper types for
billing.credit_grant events
- Fixed breaking changes: invoice.subscription path, subscription period
fields location, removed properties
## How it works
1. When `invoice.finalized` webhook is received for
`subscription_cycle`, the system:
- Calculates usage from the previous period
- Determines unused credits (tier cap - usage)
- Caps rollover at current tier cap
- Creates a Stripe credit grant with expiration at end of new period
2. When credit grants are created/updated/voided:
- Billing alerts are recreated with the updated credit balance
3. The UI displays:
- Base credits (from subscription tier)
- Rollover credits (from previous periods)
- Total available credits
## Edge Cases Handled
- Credit grant voided: `billing.credit_grant.updated` webhook triggers
alert update
- Credit grant expired: Stripe's `creditBalanceSummary` API excludes
expired grants
- No unused credits: Rollover service skips grant creation
- Customer ID as object: Controller extracts `.id` from expanded
customer
- when a view is deleted, it is removed from left menu + from list views
in dropdown
- deactivated fields are not suggested as options to group records by
for a view (only in FE, not forbidden by BE)
## Context
After flushing the Redis cache (e.g., via `cache:flush` command), users
get stuck in an infinite loop showing "Your workspace has been updated
with a new data model. Please refresh the page." - refreshing the page
doesn't help.
## Root Cause
When the cache is flushed, `getMetadataVersion()` returns `undefined`.
The version comparison in the error handler then becomes:
```typescript
requestMetadataVersion !== `${currentMetadataVersion}`
// Evaluates to: "5" !== "undefined" → always true
```
This triggers the schema mismatch error on every request, even after
page refresh.
## History
| Date | Commit | What Happened |
|------|--------|---------------|
| Aug 2024 | #6691 (`17a1760afd`) | Introduced the
`${currentMetadataVersion}` template literal that converts `undefined`
to the string `"undefined"` |
| Dec 2025 | #16536 (`0035fc1145`) | Removed the safety check that would
throw an early error when cache is empty, allowing `undefined` to
propagate |
## Fix
Add `isDefined(currentMetadataVersion)` check before comparing versions.
If the server doesn't have a cached version, we shouldn't treat it as a
mismatch - the schema factory already handles populating the cache when
it actually needs the version.
```typescript
if (
requestMetadataVersion &&
isDefined(currentMetadataVersion) && // ← Added this check
requestMetadataVersion !== `${currentMetadataVersion}`
) {
```
## Testing
1. Flush the cache: `npx nx run twenty-server:command cache:flush`
2. Refresh the page
3. Verify no infinite loop occurs and app loads normally
Closes [571](https://github.com/twentyhq/core-team-issues/issues/571).
Replicates the behavior of number field and allows specifying the number
of decimals for currency field.
<img width="931" height="858" alt="image"
src="https://github.com/user-attachments/assets/f5100d58-b1b0-4a88-a090-e98b2feeebd0"
/>
Currencies around the world have a maximum of three decimal places -
BHD, KWD etc. However, I have added a maximum of five decimal places in
case someone wants to use the currency field type for displaying things
like `per-second-billing` or `exchange rates`.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds configurable decimals (0–5) for currency fields, updating
settings UI, types/schema, display/aggregate formatting, and input
precision.
>
> - **Currency field settings**:
> - Add `decimals` (0–5) to `FieldCurrencyMetadata.settings` and
validate via `currencyFieldSettingsSchema`.
> - Update `SettingsDataModelFieldCurrencyForm` to manage `format`
(short/full) and `decimals` with counter when `full`; wire defaults via
`useCurrencySettingsFormInitialValues`.
> - **Display & aggregation**:
> - `CurrencyDisplay` and
`transformAggregateRawValueIntoAggregateDisplayValue` honor `decimals`
when `format` is `full`; keep short format using `formatToShortNumber`.
> - **Input**:
> - Increase currency `IMaskInput` precision with `scale=5`.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
959a8fc1ea. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
## Summary
This PR ensures usage alerts are created for all billing scenarios.
## Background
Per [Stripe
documentation](https://docs.stripe.com/billing/subscriptions/usage-based/alerts),
usage alerts are **one-time per customer** - they trigger once and only
consider usage reported after the alert is created. This means we need
to create a new alert whenever:
1. ✅ Subscription is created (trial) - Already implemented
2. ✅ Trial ends (user becomes Active subscriber) - **Added in this PR**
3. ✅ Credit tier changes (upgrade) - **Added in this PR**
4. ✅ **New billing cycle starts** - **Critical fix in this PR!**
## The Issue
Previously, the invoice webhook reset `hasReachedCurrentPeriodCap =
false` at cycle end, but didn't create a new alert. This meant after the
first billing period, there was no alert to trigger and users could
exceed their limit without being blocked.
## Changes
### 1. Invoice Webhook (`billing-webhook-invoice.service.ts`)
When invoice is finalized for `subscription_cycle`:
- Reset `hasReachedCurrentPeriodCap = false` ✅ (already done)
- **NEW**: Create alert at the current tier cap
### 2. End Trial Period (`billing-subscription.service.ts`)
In `endTrialPeriod()`:
- **NEW**: Create alert at the paid tier cap (not trial cap)
### 3. Credit Tier Upgrades (`billing-subscription.service.ts`)
In `changeMeteredPrice()`, when upgrading immediately (not scheduled for
period end):
- **NEW**: Reset `hasReachedCurrentPeriodCap = false`
- **NEW**: Create alert at new tier cap
### 4. Alert Title Update (`stripe-billing-alert.service.ts`)
Changed from "Trial usage cap" to "Usage cap" since alerts are now used
for all scenarios.
## Stripe Alert Limits
- Max 25 alerts per meter+customer combination
- Alerts only evaluate usage reported after creation
- One-time alerts trigger once per customer
With monthly billing cycles + occasional tier changes, we should stay
well under the 25 alert limit.
## Testing
- TypeScript typechecking passes
- Backend services properly inject new dependencies
This PR presents an attempt to increase the speed of the tests to run,
by using the same front-end built for Front and E2E CIs. It implied
merging front and E2E in one CI, but allowed to still have two different
status, and to have E2E run even if no front files have changed.
Also now using depot to build FE.
<img width="270" height="546" alt="image"
src="https://github.com/user-attachments/assets/d9eccc62-4494-4102-ad4e-241fec4bd4ea"
/>
Fixed inconsistency where List view (Group By) still showed empty groups
while Kanban hides them.
Update `visibleRecordGroupIdsComponentFamilySelector` now filters out
groups with zero records using
`recordIndexRecordIdsByGroupComponentFamilyState` and
`recordIndexShouldHideEmptyRecordGroupsComponentState`, ensuring empty
groups are consistently hidden across views.
Fixes#16212
Fixes: #16734
Solution:
The ObjectOptionsDropdownDefaultView was reading from
recordIndexFieldDefinitionsState via useObjectOptionsForBoard, while
field visibility changes update currentRecordFieldsComponentState. This
caused the "X shown" count to remain stale after hiding fields.
Changed to use visibleRecordFieldsComponentSelector (same state source
used by ViewFieldsVisibleDropdownSection) so both components react to
the same state updates.
https://github.com/user-attachments/assets/62eb5c98-f15f-4ee8-bdce-1ab4e4752f66
## Summary
This PR adds user-friendly error handling for AI chat features,
specifically for **billing credits exhausted** and **API key not
configured** errors.
## Changes
### Backend
- Added `BILLING_CREDITS_EXHAUSTED` exception code with 402 status
- Added `API_KEY_NOT_CONFIGURED` exception code with 503 status
- Added billing check before AI chat streaming in
`agent-chat.controller.ts`
- Added error code to HTTP exception response body for frontend error
type detection
- Created `AgentRestApiExceptionFilter` for agent-specific errors
### Frontend
- Created `AIChatBanner` - reusable banner component for error/warning
messages
- Created `AIChatCreditsExhaustedMessage` - shows upgrade prompts based
on user permissions
- Created `AIChatApiKeyNotConfiguredMessage` - shows configuration
guidance with docs link
- Created `AIChatErrorRenderer` - encapsulates error type switching
logic (fixes nested ternary)
- Created `AIChatStandaloneError` - displays errors when there are no
messages
- Split `aiChatErrorUtils.ts` into separate files (1 export per file):
- `AIChatErrorCode.ts`
- `extractErrorCode.ts`
- `isAIChatErrorOfType.ts`
- `isBillingCreditsExhaustedError.ts`
- `isApiKeyNotConfiguredError.ts`
- Added comprehensive test coverage (27 tests)
### Other
- Updated trial period banner messaging
## Testing
- All lint checks pass
- All 27 new tests pass
- TypeScript typecheck passes