diff --git a/apps/backoffice/src/app/features/certificate-designer/components/DesignerToolbar.tsx b/apps/backoffice/src/app/features/certificate-designer/components/DesignerToolbar.tsx
index 4ab644bc6..c34a1e368 100644
--- a/apps/backoffice/src/app/features/certificate-designer/components/DesignerToolbar.tsx
+++ b/apps/backoffice/src/app/features/certificate-designer/components/DesignerToolbar.tsx
@@ -84,9 +84,11 @@ export function DesignerToolbar({
{validityDays != null
? t('designer.validityDays', '{{count}} days', { count: validityDays })
- : t('designer.validityMonths', '{{count}} months', {
- count: validityMonths,
- })}
+ : validityMonths
+ ? t('designer.validityMonths', '{{count}} months', {
+ count: validityMonths,
+ })
+ : t('designer.validityNone', 'Does not expire')}
{t('designer.validityEditedOn', '— set on Certificate Requirements → Behaviour')}
diff --git a/apps/backoffice/src/app/features/certificate-requirements/components/BehaviorTab.tsx b/apps/backoffice/src/app/features/certificate-requirements/components/BehaviorTab.tsx
index 891acfc15..d1606b697 100644
--- a/apps/backoffice/src/app/features/certificate-requirements/components/BehaviorTab.tsx
+++ b/apps/backoffice/src/app/features/certificate-requirements/components/BehaviorTab.tsx
@@ -72,9 +72,13 @@ function toDraft(licenseType: LicenseType): Draft {
licenseType.capitalThreshold == null
? null
: Number(licenseType.capitalThreshold),
+ // Zero is a real stored state for `validityMonths` (a type that issues
+ // nothing with an expiry) and is kept. Zero in the other two is not a
+ // policy anyone set — it is an unfilled column — and seeding a box with a
+ // value below its own floor only produces a save the server rejects.
validityMonths: licenseType.validityMonths ?? 12,
- validityDays: licenseType.validityDays ?? null,
- renewalWindowDays: licenseType.renewalWindowDays ?? 60,
+ validityDays: licenseType.validityDays || null,
+ renewalWindowDays: licenseType.renewalWindowDays || 60,
expiryReminderDays: licenseType.expiryReminderDays ?? [60, 30, 7],
requiresOperatorMode: licenseType.requiresOperatorMode ?? true,
allowMultipleOpenDrafts: licenseType.allowMultipleOpenDrafts ?? false,
@@ -128,9 +132,38 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
// against an expiry that never arrives, so none of them are asked for.
const expires = draft.validityDays !== null || draft.validityMonths > 0;
+ // The server's floor for a stated term is 6 months, so no-expiry is a state
+ // this form can hold and edit around but cannot switch a type into. Offered
+ // only where it is already what the type is, rather than as an option whose
+ // save would be refused.
+ const noExpiryAvailable =
+ (licenseType.validityMonths ?? 12) === 0 && !licenseType.validityDays;
+
async function onSave() {
+ // Only the settings this form actually asked for. The endpoint patches, so
+ // an omitted field keeps its stored value — and the fields hidden above are
+ // hidden precisely because the type has no such policy, which the server
+ // stores as a zero its own validators then refuse (`validityMonths` has a
+ // floor of 6, `renewalWindowDays` of 1). Echoing those back is what made
+ // saving an ownership transfer fail outright.
+ const {
+ validityMonths,
+ validityDays,
+ renewalWindowDays,
+ expiryReminderDays,
+ ...rest
+ } = draft;
+
const ok = await run(
- () => save({ id: licenseType.id, ...draft }).unwrap(),
+ () =>
+ save({
+ id: licenseType.id,
+ ...rest,
+ ...(expires ? { validityMonths, validityDays } : {}),
+ ...(expires && draft.renewalEnabled
+ ? { renewalWindowDays, expiryReminderDays }
+ : {}),
+ }).unwrap(),
t('certReq.behavior.saved', 'Configuration saved.'),
);
if (ok) setDirty(false);
@@ -334,7 +367,14 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
data={[
{ value: 'MONTHS', label: t('certReq.behavior.unitMonths', 'Months') },
{ value: 'DAYS', label: t('certReq.behavior.unitDays', 'Days') },
- { value: 'NONE', label: t('certReq.behavior.unitNone', 'Does not expire') },
+ ...(noExpiryAvailable
+ ? [
+ {
+ value: 'NONE',
+ label: t('certReq.behavior.unitNone', 'Does not expire'),
+ },
+ ]
+ : []),
]}
value={
draft.validityDays !== null
diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts
index 543417370..5b6e36fbe 100644
--- a/apps/backoffice/src/app/i18n/locales/am.ts
+++ b/apps/backoffice/src/app/i18n/locales/am.ts
@@ -1253,6 +1253,7 @@ export const am: Translations = {
validityMonths_other: "{{count}} ወራት",
validityDays_one: "{{count}} ቀን",
validityDays_other: "{{count}} ቀናት",
+ validityNone: "ጊዜው አያልፍም",
validityEditedOn: "— በምስክር ወረቀት መስፈርቶች → ባህሪ ውስጥ ይዘጋጃል",
newVersion: "አዲስ ስሪት",
versions: "ስሪቶች",
diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts
index a3aed8a28..efe91b783 100644
--- a/apps/backoffice/src/app/i18n/locales/en.ts
+++ b/apps/backoffice/src/app/i18n/locales/en.ts
@@ -1260,6 +1260,7 @@ export const en = {
validityMonths_other: '{{count}} months',
validityDays_one: '{{count}} day',
validityDays_other: '{{count}} days',
+ validityNone: 'Does not expire',
validityEditedOn: '— set on Certificate Requirements → Behaviour',
newVersion: 'New version',
versions: 'Versions',
diff --git a/apps/portal/src/app/features/licensing/components/LicenseCatalogue.tsx b/apps/portal/src/app/features/licensing/components/LicenseCatalogue.tsx
index c948e5e58..189eb8f00 100644
--- a/apps/portal/src/app/features/licensing/components/LicenseCatalogue.tsx
+++ b/apps/portal/src/app/features/licensing/components/LicenseCatalogue.tsx
@@ -322,8 +322,16 @@ function LicenseTypeCard({
)}
{type.issuesCertificate ? (
+ // A term in days wins over the months column, and a type with
+ // neither issues something that simply does not expire — a
+ // transfer, or any one-off record. Reading `validityMonths`
+ // alone badged those "0 months".
- {t('licensing.catalogue.validityBadge', { months: type.validityMonths })}
+ {type.validityDays
+ ? t('licensing.catalogue.validityDaysBadge', { count: type.validityDays })
+ : type.validityMonths
+ ? t('licensing.catalogue.validityBadge', { months: type.validityMonths })
+ : t('licensing.catalogue.noExpiryBadge')}
) : (
diff --git a/apps/portal/src/app/i18n/locales/am.ts b/apps/portal/src/app/i18n/locales/am.ts
index fdc789666..a52fe60b6 100644
--- a/apps/portal/src/app/i18n/locales/am.ts
+++ b/apps/portal/src/app/i18n/locales/am.ts
@@ -884,6 +884,9 @@ export const am: Translations = {
capitalTooltip: 'በባንክ ደብዳቤ መረጋገጥ ያለበት ዝቅተኛ ካፒታል',
capitalBadge: 'ካፒታል {{amount}}',
validityBadge: '{{months}} ወራት',
+ validityDaysBadge_one: '{{count}} ቀን',
+ validityDaysBadge_other: '{{count}} ቀናት',
+ noExpiryBadge: 'ጊዜው አያልፍም',
evaluationTooltip: 'በምስክር ወረቀት ፈንታ በባለሥልጣኑ ውሳኔ የሚጠናቀቅ',
evaluationOnly: 'ግምገማ ብቻ',
startApplication: 'ማመልከቻ ጀምር',
diff --git a/apps/portal/src/app/i18n/locales/en.ts b/apps/portal/src/app/i18n/locales/en.ts
index 3e52108bb..14ba1720c 100644
--- a/apps/portal/src/app/i18n/locales/en.ts
+++ b/apps/portal/src/app/i18n/locales/en.ts
@@ -886,6 +886,9 @@ export const en = {
capitalTooltip: 'Minimum capital that must be evidenced by a bank letter',
capitalBadge: 'Capital {{amount}}',
validityBadge: '{{months}} months',
+ validityDaysBadge_one: '{{count}} day',
+ validityDaysBadge_other: '{{count}} days',
+ noExpiryBadge: 'No expiry',
evaluationTooltip: 'Concludes with an EMA decision rather than a certificate',
evaluationOnly: 'Evaluation only',
startApplication: 'Start application',