fix(course): coerce metadata.discount to number before save (#830)
* fix(course): coerce metadata.discount to number before save Normalize string discount values from the landing page pricing form and coerce legacy string metadata on ZCourseUpdate validation so course settings saves do not fail with a Zod type error. Fixes classroomio/classroomio#828 Co-authored-by: Rotimi Best <[email protected]> * refactor(utils): add shared toFiniteNumber for metadata discount coercion Co-authored-by: Rotimi Best <[email protected]> * fix(utils): only coerce string discount values in metadata preprocess Reject broad Number() coercion on booleans and arrays. String and finite number discounts are normalized; invalid types are left for Zod to reject. Addresses Greptile review on #830 Co-authored-by: Rotimi Best <[email protected]> * refactor(dashboard): use writable $derived in pricing form again Keep toFiniteNumber coercion in setter effect; derived fields stay in sync when course loads or updates (Svelte 5 writable derived). Co-authored-by: Rotimi Best <[email protected]> * fix(utils): reject invalid metadata.discount instead of clearing it Only coerce numeric strings in preprocess; leave invalid strings, non-finite numbers, and other types for Zod to reject. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
0c295580f1
commit
ee4d4e031d
+5
-4
@@ -7,6 +7,7 @@
|
|||||||
import type { Course } from '$features/course/utils/types';
|
import type { Course } from '$features/course/utils/types';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { isCourseFree } from '$lib/utils/functions/course';
|
import { isCourseFree } from '$lib/utils/functions/course';
|
||||||
|
import { toFiniteNumber } from '@cio/utils/functions';
|
||||||
|
|
||||||
import { InputField } from '@cio/ui/custom/input-field';
|
import { InputField } from '@cio/ui/custom/input-field';
|
||||||
import { TextEditor } from '$features/ui';
|
import { TextEditor } from '$features/ui';
|
||||||
@@ -18,10 +19,10 @@
|
|||||||
|
|
||||||
let { course = $bindable(), setter }: Props = $props();
|
let { course = $bindable(), setter }: Props = $props();
|
||||||
|
|
||||||
let discount = $derived(get(course, 'metadata.discount', 0));
|
|
||||||
let paymentLink = $derived(get(course, 'metadata.paymentLink', ''));
|
let paymentLink = $derived(get(course, 'metadata.paymentLink', ''));
|
||||||
let showDiscount = $derived(get(course, 'metadata.showDiscount', false));
|
let showDiscount = $derived(Boolean(get(course, 'metadata.showDiscount', false)));
|
||||||
let giftToggled = $derived(get(course, 'metadata.reward.show', false));
|
let discount = $derived(toFiniteNumber(get(course, 'metadata.discount', 0)) ?? 0);
|
||||||
|
let giftToggled = $derived(Boolean(get(course, 'metadata.reward.show', false)));
|
||||||
|
|
||||||
function handleChange(content: string) {
|
function handleChange(content: string) {
|
||||||
setter(content, 'metadata.reward.description');
|
setter(content, 'metadata.reward.description');
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
setter(paymentLink, 'metadata.paymentLink');
|
setter(paymentLink, 'metadata.paymentLink');
|
||||||
});
|
});
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
setter(discount, 'metadata.discount');
|
setter(toFiniteNumber(discount) ?? 0, 'metadata.discount');
|
||||||
});
|
});
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
setter(giftToggled, 'metadata.reward.show');
|
setter(giftToggled, 'metadata.reward.show');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './array';
|
export * from './array';
|
||||||
export * from './currency';
|
export * from './currency';
|
||||||
export * from './fileValidation';
|
export * from './fileValidation';
|
||||||
|
export * from './number';
|
||||||
export * from './sanitize';
|
export * from './sanitize';
|
||||||
export * from './slug';
|
export * from './slug';
|
||||||
export * from './transcript-vtt';
|
export * from './transcript-vtt';
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Coerces a number or numeric string to a finite number.
|
||||||
|
* Does not coerce booleans, arrays, or other types (unlike bare `Number()`).
|
||||||
|
* Empty, null, and invalid values return undefined.
|
||||||
|
*/
|
||||||
|
export function toFiniteNumber(value: unknown): number | undefined {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { toFiniteNumber } from '../../functions/number';
|
||||||
import { ALLOWED_CONTENT_TYPES, ALLOWED_DOCUMENT_TYPES } from '../constants';
|
import { ALLOWED_CONTENT_TYPES, ALLOWED_DOCUMENT_TYPES } from '../constants';
|
||||||
import { ZCourseCalloutInput } from './callout';
|
import { ZCourseCalloutInput } from './callout';
|
||||||
import { ZCourseType } from './course-type';
|
import { ZCourseType } from './course-type';
|
||||||
@@ -321,6 +322,15 @@ function preprocessCourseMetadata(value: unknown): unknown {
|
|||||||
metadata.allowNewStudent = true;
|
metadata.allowNewStudent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata.discount === null || metadata.discount === '') {
|
||||||
|
metadata.discount = undefined;
|
||||||
|
} else if (typeof metadata.discount === 'string') {
|
||||||
|
const parsedDiscount = toFiniteNumber(metadata.discount);
|
||||||
|
if (parsedDiscount !== undefined) {
|
||||||
|
metadata.discount = parsedDiscount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user