richie-education 3.1.3-dev8 → 3.2.1-dev1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/__mocks__/utils/context.ts +4 -0
- package/i18n/locales/ar-SA.json +30 -10
- package/i18n/locales/es-ES.json +30 -10
- package/i18n/locales/fa-IR.json +30 -10
- package/i18n/locales/fr-CA.json +31 -11
- package/i18n/locales/fr-FR.json +32 -12
- package/i18n/locales/ko-KR.json +30 -10
- package/i18n/locales/pt-PT.json +30 -10
- package/i18n/locales/ru-RU.json +30 -10
- package/i18n/locales/vi-VN.json +30 -10
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +18 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +4 -0
- package/js/components/CourseGlimpse/utils.ts +35 -30
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -3
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +131 -64
- package/js/components/SaleTunnel/index.stories.tsx +17 -2
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/settings/index.ts +1 -0
- package/js/settings/settings.prod.ts +1 -0
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Course.ts +4 -0
- package/js/types/Joanie.ts +31 -22
- package/js/types/index.ts +6 -2
- package/js/utils/test/factories/joanie.ts +18 -11
- package/js/utils/test/factories/richie.ts +10 -2
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/Slider/components/SlidePanel.tsx +9 -0
- package/js/widgets/Slider/index.stories.tsx +53 -0
- package/js/widgets/Slider/index.tsx +21 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +8 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +116 -75
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +29 -30
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +36 -2
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +36 -2
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +58 -8
- package/package.json +2 -1
- package/scss/colors/_theme.scss +3 -0
- package/scss/components/templates/richie/slider/_slider.scss +19 -0
- package/scss/objects/_blogpost_glimpses.scss +5 -0
- package/scss/objects/_course_glimpses.scss +16 -0
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
package/i18n/locales/vi-VN.json
CHANGED
|
@@ -227,6 +227,18 @@
|
|
|
227
227
|
"description": "Course run languages",
|
|
228
228
|
"message": "Available in {languages}"
|
|
229
229
|
},
|
|
230
|
+
"components.CourseProductItem.discount_rate": {
|
|
231
|
+
"description": "Discount rate information",
|
|
232
|
+
"message": "-{rate}%"
|
|
233
|
+
},
|
|
234
|
+
"components.CourseProductItem.discounted_price": {
|
|
235
|
+
"description": "Label for the discounted price of a product",
|
|
236
|
+
"message": "Discounted price:"
|
|
237
|
+
},
|
|
238
|
+
"components.CourseProductItem.from": {
|
|
239
|
+
"description": "Discount start date information",
|
|
240
|
+
"message": "from {from}"
|
|
241
|
+
},
|
|
230
242
|
"components.CourseProductItem.fromTo": {
|
|
231
243
|
"description": "Course run date range",
|
|
232
244
|
"message": "From {from} {to, select, undefined {} other {to {to}}}"
|
|
@@ -243,10 +255,18 @@
|
|
|
243
255
|
"description": "Message displayed when no seats are available for the product",
|
|
244
256
|
"message": "Sorry, no seats available for now"
|
|
245
257
|
},
|
|
258
|
+
"components.CourseProductItem.original_price": {
|
|
259
|
+
"description": "Label for the original price of a product",
|
|
260
|
+
"message": "Original price:"
|
|
261
|
+
},
|
|
246
262
|
"components.CourseProductItem.purchased": {
|
|
247
263
|
"description": "Message displayed when authenticated user owned the product",
|
|
248
264
|
"message": "Purchased"
|
|
249
265
|
},
|
|
266
|
+
"components.CourseProductItem.to": {
|
|
267
|
+
"description": "Discount end date information",
|
|
268
|
+
"message": "to {to}"
|
|
269
|
+
},
|
|
250
270
|
"components.CourseProductsList.end": {
|
|
251
271
|
"description": "End label displayed in the header of course run dates section",
|
|
252
272
|
"message": "End"
|
|
@@ -1927,8 +1947,8 @@
|
|
|
1927
1947
|
"description": "Sub title of the dashboard sidebar",
|
|
1928
1948
|
"message": "You are on your teacher dashboard"
|
|
1929
1949
|
},
|
|
1930
|
-
"components.TeacherDashboardTraining.
|
|
1931
|
-
"description": "Message displayed when requested
|
|
1950
|
+
"components.TeacherDashboardTraining.errorNoOffering": {
|
|
1951
|
+
"description": "Message displayed when requested offering is not found",
|
|
1932
1952
|
"message": "This product doesn't exist"
|
|
1933
1953
|
},
|
|
1934
1954
|
"components.TeacherDashboardTrainingLoader.loading": {
|
|
@@ -2047,14 +2067,6 @@
|
|
|
2047
2067
|
"description": "Error message shown to the user when orders fetch request fails.",
|
|
2048
2068
|
"message": "An error occurred while fetching orders. Please retry later."
|
|
2049
2069
|
},
|
|
2050
|
-
"hooks.useCourseProductRelations.errorGet": {
|
|
2051
|
-
"description": "Error message shown to the user when course product relation fetch request fails.",
|
|
2052
|
-
"message": "An error occurred while fetching trainings. Please retry later."
|
|
2053
|
-
},
|
|
2054
|
-
"hooks.useCourseProductRelations.errorNotFound": {
|
|
2055
|
-
"description": "Error message shown to the user when no course product relation matches.",
|
|
2056
|
-
"message": "Cannot find the training."
|
|
2057
|
-
},
|
|
2058
2070
|
"hooks.useCourseProductUnion.errorGet": {
|
|
2059
2071
|
"description": "Error message shown to the user when trainings fetch request fails.",
|
|
2060
2072
|
"message": "An error occurred while fetching trainings. Please retry later."
|
|
@@ -2147,6 +2159,14 @@
|
|
|
2147
2159
|
"description": "Error message shown to the user when it isn't logged.",
|
|
2148
2160
|
"message": "You aren't logged in."
|
|
2149
2161
|
},
|
|
2162
|
+
"hooks.useOfferings.errorGet": {
|
|
2163
|
+
"description": "Error message shown to the user when offering fetch request fails.",
|
|
2164
|
+
"message": "An error occurred while fetching trainings. Please retry later."
|
|
2165
|
+
},
|
|
2166
|
+
"hooks.useOfferings.errorNotFound": {
|
|
2167
|
+
"description": "Error message shown to the user when no offering matches.",
|
|
2168
|
+
"message": "Cannot find the training."
|
|
2169
|
+
},
|
|
2150
2170
|
"hooks.useOpenEdxProfile.errorGet": {
|
|
2151
2171
|
"description": "Error message shown to the user when openEdx profile fetch request fails.",
|
|
2152
2172
|
"message": "An error occurred while fetching your profile. Please retry later."
|
package/js/api/joanie.ts
CHANGED
|
@@ -127,8 +127,8 @@ export const getRoutes = () => {
|
|
|
127
127
|
},
|
|
128
128
|
organizations: {
|
|
129
129
|
get: `${baseUrl}/organizations/:id/`,
|
|
130
|
-
|
|
131
|
-
get: `${baseUrl}/organizations/:organization_id/
|
|
130
|
+
offerings: {
|
|
131
|
+
get: `${baseUrl}/organizations/:organization_id/offerings/:id/`,
|
|
132
132
|
},
|
|
133
133
|
courses: {
|
|
134
134
|
get: `${baseUrl}/organizations/:organization_id/courses/:id/`,
|
|
@@ -156,8 +156,8 @@ export const getRoutes = () => {
|
|
|
156
156
|
courseRuns: {
|
|
157
157
|
get: `${baseUrl}/course-runs/:id/`,
|
|
158
158
|
},
|
|
159
|
-
|
|
160
|
-
get: `${baseUrl}/
|
|
159
|
+
offerings: {
|
|
160
|
+
get: `${baseUrl}/offerings/:id/`,
|
|
161
161
|
},
|
|
162
162
|
contractDefinitions: {
|
|
163
163
|
previewTemplate: `${baseUrl}/contract_definitions/:id/preview_template/`,
|
|
@@ -470,12 +470,12 @@ const API = (): Joanie.API => {
|
|
|
470
470
|
).then(checkStatus);
|
|
471
471
|
},
|
|
472
472
|
},
|
|
473
|
-
|
|
474
|
-
get: (filters?: Joanie.
|
|
473
|
+
offerings: {
|
|
474
|
+
get: (filters?: Joanie.OfferingQueryFilters) => {
|
|
475
475
|
return fetchWithJWT(
|
|
476
476
|
filters?.organization_id
|
|
477
|
-
? buildApiUrl(ROUTES.organizations.
|
|
478
|
-
: buildApiUrl(ROUTES.
|
|
477
|
+
? buildApiUrl(ROUTES.organizations.offerings.get, filters)
|
|
478
|
+
: buildApiUrl(ROUTES.offerings.get, filters),
|
|
479
479
|
).then(checkStatus);
|
|
480
480
|
},
|
|
481
481
|
},
|
|
@@ -6,14 +6,10 @@ import { IntlProvider } from 'react-intl';
|
|
|
6
6
|
import fetchMock from 'fetch-mock';
|
|
7
7
|
import { QueryStateFactory } from 'utils/test/factories/reactQuery';
|
|
8
8
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
-
import {
|
|
10
|
-
ContractFactory,
|
|
11
|
-
CourseProductRelationFactory,
|
|
12
|
-
OrganizationFactory,
|
|
13
|
-
} from 'utils/test/factories/joanie';
|
|
9
|
+
import { ContractFactory, OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
14
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
15
11
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
16
|
-
import {
|
|
12
|
+
import { isOffering } from 'types/Joanie';
|
|
17
13
|
import { Props } from './AbstractContractFrame';
|
|
18
14
|
import { OrganizationContractFrame } from '.';
|
|
19
15
|
|
|
@@ -77,31 +73,29 @@ describe('OrganizationContractFrame', () => {
|
|
|
77
73
|
|
|
78
74
|
it.each([
|
|
79
75
|
{
|
|
80
|
-
label: 'contractList: undefined,
|
|
76
|
+
label: 'contractList: undefined, offering: undefined',
|
|
81
77
|
contractList: undefined,
|
|
82
|
-
|
|
78
|
+
offering: undefined,
|
|
83
79
|
},
|
|
84
80
|
{
|
|
85
|
-
label: 'contractList: 2 Contract,
|
|
81
|
+
label: 'contractList: 2 Contract, offering: undefined',
|
|
86
82
|
contractList: ContractFactory().many(2),
|
|
87
|
-
|
|
83
|
+
offering: undefined,
|
|
88
84
|
},
|
|
89
85
|
{
|
|
90
|
-
label: 'contractList: undefined,
|
|
86
|
+
label: 'contractList: undefined, offering: one Offering',
|
|
91
87
|
contractList: undefined,
|
|
92
|
-
|
|
88
|
+
offering: OfferingFactory().one(),
|
|
93
89
|
},
|
|
94
90
|
])(
|
|
95
91
|
'should implement AbstractContractFrame for organization and $label',
|
|
96
|
-
async ({ contractList,
|
|
92
|
+
async ({ contractList, offering }) => {
|
|
97
93
|
const organization = OrganizationFactory().one();
|
|
98
94
|
const contracts = contractList || ContractFactory().many(2);
|
|
99
95
|
const isOpen = faker.datatype.boolean();
|
|
100
96
|
|
|
101
97
|
const invitationLinkQueryString =
|
|
102
|
-
|
|
103
|
-
? `?course_product_relation_ids=${courseProductRelation.id}`
|
|
104
|
-
: '';
|
|
98
|
+
offering && isOffering(offering) ? `?offering_ids=${offering.id}` : '';
|
|
105
99
|
const expectedUrls = {
|
|
106
100
|
getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
|
|
107
101
|
checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
|
|
@@ -133,9 +127,7 @@ describe('OrganizationContractFrame', () => {
|
|
|
133
127
|
<Wrapper client={client}>
|
|
134
128
|
<OrganizationContractFrame
|
|
135
129
|
organizationId={organization.id}
|
|
136
|
-
|
|
137
|
-
courseProductRelation ? [courseProductRelation.id] : undefined
|
|
138
|
-
}
|
|
130
|
+
offeringIds={offering ? [offering.id] : undefined}
|
|
139
131
|
isOpen={isOpen}
|
|
140
132
|
onDone={handleDone}
|
|
141
133
|
onClose={handleClose}
|
|
@@ -4,17 +4,17 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
|
4
4
|
import AbstractContractFrame, {
|
|
5
5
|
AbstractProps,
|
|
6
6
|
} from 'components/ContractFrame/AbstractContractFrame';
|
|
7
|
-
import { Contract,
|
|
7
|
+
import { Contract, Offering } from 'types/Joanie';
|
|
8
8
|
|
|
9
9
|
interface Props extends AbstractProps {
|
|
10
10
|
contractIds?: Contract['id'][];
|
|
11
11
|
organizationId: string;
|
|
12
|
-
|
|
12
|
+
offeringIds?: Offering['id'][];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const OrganizationContractFrame = ({
|
|
16
16
|
organizationId,
|
|
17
|
-
|
|
17
|
+
offeringIds = [],
|
|
18
18
|
contractIds,
|
|
19
19
|
onDone,
|
|
20
20
|
...props
|
|
@@ -29,7 +29,7 @@ const OrganizationContractFrame = ({
|
|
|
29
29
|
be signed. We need to keep track of these ids to check if all contracts have been signed.
|
|
30
30
|
*/
|
|
31
31
|
const response = await api.organizations.contracts.getSignatureLinks({
|
|
32
|
-
|
|
32
|
+
offering_ids: offeringIds,
|
|
33
33
|
organization_id: organizationId,
|
|
34
34
|
contracts_ids: contractIds,
|
|
35
35
|
});
|
|
@@ -65,6 +65,35 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
|
|
|
65
65
|
const offerIcon = `icon-offer-${offer}` as OfferIconType;
|
|
66
66
|
const offerCertificateIcon = hasCertificateOffer && IconTypeEnum.SCHOOL;
|
|
67
67
|
const offerPrice = hasEnrollmentOffer && course.price;
|
|
68
|
+
const discountedPrice = course.discounted_price ?? null;
|
|
69
|
+
const hasDiscount = discountedPrice !== null;
|
|
70
|
+
|
|
71
|
+
let $price = null;
|
|
72
|
+
|
|
73
|
+
if (offerPrice) {
|
|
74
|
+
if (hasDiscount) {
|
|
75
|
+
$price = (
|
|
76
|
+
<div className="offer_prices">
|
|
77
|
+
<span className="offer__price offer__price--striked">
|
|
78
|
+
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
79
|
+
</span>
|
|
80
|
+
<span className="offer__price offer__price--discounted">
|
|
81
|
+
<FormattedNumber
|
|
82
|
+
value={discountedPrice}
|
|
83
|
+
currency={course.price_currency}
|
|
84
|
+
style="currency"
|
|
85
|
+
/>
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
$price = (
|
|
91
|
+
<span className="offer__price">
|
|
92
|
+
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
68
97
|
|
|
69
98
|
return (
|
|
70
99
|
<div className="course-glimpse-footer">
|
|
@@ -99,11 +128,7 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
|
|
|
99
128
|
name={offerIcon}
|
|
100
129
|
title={intl.formatMessage(courseOfferMessages[offer])}
|
|
101
130
|
/>
|
|
102
|
-
{
|
|
103
|
-
<span className="offer__price">
|
|
104
|
-
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
105
|
-
</span>
|
|
106
|
-
)}
|
|
131
|
+
{$price}
|
|
107
132
|
</div>
|
|
108
133
|
</div>
|
|
109
134
|
);
|
|
@@ -59,6 +59,10 @@ describe('widgets/Search/components/CourseGlimpse', () => {
|
|
|
59
59
|
certificate_offer: CourseCertificateOffer.FREE,
|
|
60
60
|
certificate_price: null,
|
|
61
61
|
price_currency: 'EUR',
|
|
62
|
+
discounted_price: null,
|
|
63
|
+
discount: null,
|
|
64
|
+
certificate_discounted_price: null,
|
|
65
|
+
certificate_discount: null,
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
const contextProps: CommonDataProps['context'] = RichieContextFactory().one();
|
|
@@ -168,6 +172,20 @@ describe('widgets/Search/components/CourseGlimpse', () => {
|
|
|
168
172
|
);
|
|
169
173
|
});
|
|
170
174
|
|
|
175
|
+
it('renders a course glimpse with a discount', () => {
|
|
176
|
+
const { container } = renderCourseGlimpse({
|
|
177
|
+
contextProps,
|
|
178
|
+
course: { ...course, price: 100.0, discount: '30%', discounted_price: 70.0 },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const prices = container.getElementsByClassName('offer_prices');
|
|
182
|
+
expect(prices.length).toBe(1);
|
|
183
|
+
expect(prices[0].children.length).toBe(2);
|
|
184
|
+
const discountedPrice = container.getElementsByClassName('offer__price--discounted');
|
|
185
|
+
expect(discountedPrice.length).toBe(1);
|
|
186
|
+
expect(discountedPrice[0]).toHaveTextContent('€70.00');
|
|
187
|
+
});
|
|
188
|
+
|
|
171
189
|
it('does not show certificate offer if the course does not offer a certificate', () => {
|
|
172
190
|
const { container } = renderCourseGlimpse({
|
|
173
191
|
contextProps,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CourseLightFactory, RichieContextFactory } from 'utils/test/factories/richie';
|
|
3
|
+
import { CourseGlimpse, getCourseGlimpseProps } from 'components/CourseGlimpse';
|
|
4
|
+
import { CourseCertificateOffer, CourseOffer } from 'types/Course';
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
component: CourseGlimpse,
|
|
@@ -8,9 +9,79 @@ export default {
|
|
|
8
9
|
|
|
9
10
|
type Story = StoryObj<typeof CourseGlimpse>;
|
|
10
11
|
|
|
12
|
+
const richieContext = RichieContextFactory().one();
|
|
13
|
+
const courseLight = CourseLightFactory().one();
|
|
14
|
+
const courseGlimpseCourse = getCourseGlimpseProps(courseLight);
|
|
15
|
+
|
|
11
16
|
export const RichieCourse: Story = {
|
|
12
17
|
args: {
|
|
13
|
-
context:
|
|
14
|
-
course:
|
|
18
|
+
context: richieContext,
|
|
19
|
+
course: { ...courseGlimpseCourse },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const certificateProduct: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
context: richieContext,
|
|
26
|
+
course: {
|
|
27
|
+
...courseGlimpseCourse,
|
|
28
|
+
title: 'Certificate Product',
|
|
29
|
+
offer: CourseOffer.FREE,
|
|
30
|
+
price: null,
|
|
31
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
32
|
+
certificate_price: 100,
|
|
33
|
+
discounted_price: null,
|
|
34
|
+
discount: null,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const certificateProductDiscount: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
context: richieContext,
|
|
42
|
+
course: {
|
|
43
|
+
...courseGlimpseCourse,
|
|
44
|
+
title: 'Certificate Product with Discount',
|
|
45
|
+
offer: CourseOffer.FREE,
|
|
46
|
+
price: null,
|
|
47
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
48
|
+
certificate_price: 100,
|
|
49
|
+
discounted_price: 80,
|
|
50
|
+
discount: '-20 €',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const credentialProduct: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
context: richieContext,
|
|
58
|
+
course: {
|
|
59
|
+
...courseGlimpseCourse,
|
|
60
|
+
title: 'Credential Product',
|
|
61
|
+
icon: null,
|
|
62
|
+
offer: CourseOffer.PAID,
|
|
63
|
+
price: 100,
|
|
64
|
+
certificate_offer: null,
|
|
65
|
+
certificate_price: null,
|
|
66
|
+
discounted_price: null,
|
|
67
|
+
discount: null,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const credentialProductDiscount: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
context: richieContext,
|
|
75
|
+
course: {
|
|
76
|
+
...courseGlimpseCourse,
|
|
77
|
+
title: 'Credential Product with Discount',
|
|
78
|
+
icon: null,
|
|
79
|
+
offer: CourseOffer.PAID,
|
|
80
|
+
price: 100,
|
|
81
|
+
certificate_offer: null,
|
|
82
|
+
certificate_price: null,
|
|
83
|
+
discounted_price: 80,
|
|
84
|
+
discount: '-20 €',
|
|
85
|
+
},
|
|
15
86
|
},
|
|
16
87
|
};
|
|
@@ -47,6 +47,10 @@ export interface CourseGlimpseCourse {
|
|
|
47
47
|
certificate_price: Nullable<number>;
|
|
48
48
|
price: Nullable<number>;
|
|
49
49
|
price_currency: string;
|
|
50
|
+
discounted_price: Nullable<number>;
|
|
51
|
+
discount: Nullable<string>;
|
|
52
|
+
certificate_discounted_price: Nullable<number>;
|
|
53
|
+
certificate_discount: Nullable<string>;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export interface CourseGlimpseProps {
|
|
@@ -8,21 +8,21 @@ import {
|
|
|
8
8
|
} from 'types/Course';
|
|
9
9
|
import {
|
|
10
10
|
CourseListItem as JoanieCourse,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
OfferingLight,
|
|
12
|
+
isOffering,
|
|
13
13
|
ProductType,
|
|
14
14
|
} from 'types/Joanie';
|
|
15
15
|
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
|
|
16
16
|
import { CourseGlimpseCourse } from '.';
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const getCourseGlimpsePropsFromOffering = (
|
|
19
|
+
offering: OfferingLight,
|
|
20
20
|
intl: IntlShape,
|
|
21
21
|
organizationId?: string,
|
|
22
22
|
): CourseGlimpseCourse => {
|
|
23
23
|
const courseRouteParams = {
|
|
24
|
-
courseId:
|
|
25
|
-
|
|
24
|
+
courseId: offering.course.id,
|
|
25
|
+
offeringId: offering.id,
|
|
26
26
|
};
|
|
27
27
|
const courseRoute = organizationId
|
|
28
28
|
? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
|
|
@@ -31,35 +31,32 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
|
|
|
31
31
|
})
|
|
32
32
|
: generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
|
|
33
33
|
return {
|
|
34
|
-
id:
|
|
35
|
-
code:
|
|
36
|
-
title:
|
|
37
|
-
cover_image:
|
|
34
|
+
id: offering.id,
|
|
35
|
+
code: offering.course.code,
|
|
36
|
+
title: offering.product.title,
|
|
37
|
+
cover_image: offering.course.cover
|
|
38
38
|
? {
|
|
39
|
-
src:
|
|
39
|
+
src: offering.course.cover.src,
|
|
40
40
|
}
|
|
41
41
|
: null,
|
|
42
42
|
organization: {
|
|
43
|
-
title:
|
|
44
|
-
image:
|
|
43
|
+
title: offering.organizations[0].title,
|
|
44
|
+
image: offering.organizations[0].logo || null,
|
|
45
45
|
},
|
|
46
|
-
product_id:
|
|
46
|
+
product_id: offering.product.id,
|
|
47
47
|
course_route: courseRoute,
|
|
48
|
-
state:
|
|
48
|
+
state: offering.product.state,
|
|
49
49
|
certificate_offer:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: null,
|
|
53
|
-
offer: courseProductRelation.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
|
|
50
|
+
offering.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
|
|
51
|
+
offer: offering.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
|
|
54
52
|
certificate_price:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
price_currency: courseProductRelation.product.price_currency,
|
|
53
|
+
offering.product.type === ProductType.CERTIFICATE ? offering.product.price : null,
|
|
54
|
+
price: offering.product.type === ProductType.CREDENTIAL ? offering.product.price : null,
|
|
55
|
+
price_currency: offering.product.price_currency,
|
|
56
|
+
discounted_price: offering.product.discounted_price || null,
|
|
57
|
+
discount: offering.product.discount || null,
|
|
58
|
+
certificate_discounted_price: offering.product.certificate_discounted_price || null,
|
|
59
|
+
certificate_discount: offering.product.certificate_discount || null,
|
|
63
60
|
};
|
|
64
61
|
};
|
|
65
62
|
|
|
@@ -84,6 +81,10 @@ const getCourseGlimpsePropsFromRichieCourse = (course: RichieCourse): CourseGlim
|
|
|
84
81
|
certificate_offer: course.certificate_offer,
|
|
85
82
|
offer: course.offer,
|
|
86
83
|
certificate_price: course.certificate_price,
|
|
84
|
+
certificate_discounted_price: course.certificate_discounted_price,
|
|
85
|
+
certificate_discount: course.certificate_discount,
|
|
86
|
+
discounted_price: course.discounted_price,
|
|
87
|
+
discount: course.discount,
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
const getCourseGlimpsePropsFromJoanieCourse = (
|
|
@@ -121,16 +122,20 @@ const getCourseGlimpsePropsFromJoanieCourse = (
|
|
|
121
122
|
certificate_offer: null,
|
|
122
123
|
offer: null,
|
|
123
124
|
certificate_price: null,
|
|
125
|
+
discounted_price: null,
|
|
126
|
+
discount: null,
|
|
127
|
+
certificate_discounted_price: null,
|
|
128
|
+
certificate_discount: null,
|
|
124
129
|
};
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
export const getCourseGlimpseProps = (
|
|
128
|
-
course: RichieCourse | (JoanieCourse |
|
|
133
|
+
course: RichieCourse | (JoanieCourse | OfferingLight),
|
|
129
134
|
intl?: IntlShape,
|
|
130
135
|
organizationId?: string,
|
|
131
136
|
): CourseGlimpseCourse => {
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
137
|
+
if (isOffering(course)) {
|
|
138
|
+
return getCourseGlimpsePropsFromOffering(course, intl!, organizationId);
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
if (isRichieCourse(course)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IntlShape } from 'react-intl';
|
|
2
|
-
import {
|
|
2
|
+
import { OfferingLight, CourseListItem as JoanieCourse } from 'types/Joanie';
|
|
3
3
|
import { Course as RichieCourse } from 'types/Course';
|
|
4
4
|
import { CourseGlimpseCourse, getCourseGlimpseProps } from 'components/CourseGlimpse';
|
|
5
5
|
|
|
6
6
|
export const getCourseGlimpseListProps = (
|
|
7
|
-
courses: RichieCourse[] | (JoanieCourse |
|
|
7
|
+
courses: RichieCourse[] | (JoanieCourse | OfferingLight)[],
|
|
8
8
|
intl?: IntlShape,
|
|
9
9
|
organizationId?: string,
|
|
10
10
|
): CourseGlimpseCourse[] => {
|
|
@@ -42,7 +42,7 @@ const messages = defineMessages({
|
|
|
42
42
|
|
|
43
43
|
interface PurchaseButtonPropsBase {
|
|
44
44
|
product: Joanie.CredentialProduct | Joanie.CertificateProduct;
|
|
45
|
-
|
|
45
|
+
offering?: Joanie.Offering;
|
|
46
46
|
isWithdrawable: boolean;
|
|
47
47
|
disabled?: boolean;
|
|
48
48
|
className?: string;
|
|
@@ -66,7 +66,7 @@ interface CertificatePurchaseButtonProps extends PurchaseButtonPropsBase {
|
|
|
66
66
|
const PurchaseButton = ({
|
|
67
67
|
product,
|
|
68
68
|
course,
|
|
69
|
-
|
|
69
|
+
offering,
|
|
70
70
|
enrollment,
|
|
71
71
|
isWithdrawable,
|
|
72
72
|
organizations,
|
|
@@ -140,7 +140,7 @@ const PurchaseButton = ({
|
|
|
140
140
|
{...saleTunnelModal}
|
|
141
141
|
product={product}
|
|
142
142
|
organizations={organizations}
|
|
143
|
-
|
|
143
|
+
offering={offering}
|
|
144
144
|
enrollment={enrollment}
|
|
145
145
|
course={course}
|
|
146
146
|
isWithdrawable={isWithdrawable}
|
|
@@ -12,7 +12,8 @@ import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSpo
|
|
|
12
12
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
13
13
|
import {
|
|
14
14
|
Address,
|
|
15
|
-
|
|
15
|
+
Enrollment,
|
|
16
|
+
Offering,
|
|
16
17
|
CreditCard,
|
|
17
18
|
Order,
|
|
18
19
|
OrderState,
|
|
@@ -33,7 +34,8 @@ export interface SaleTunnelContextType {
|
|
|
33
34
|
order?: Order;
|
|
34
35
|
product: Product;
|
|
35
36
|
webAnalyticsEventKey: string;
|
|
36
|
-
|
|
37
|
+
offering?: Offering;
|
|
38
|
+
enrollment?: Enrollment;
|
|
37
39
|
|
|
38
40
|
// internal
|
|
39
41
|
step: SaleTunnelStep;
|
|
@@ -121,7 +123,8 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
121
123
|
webAnalyticsEventKey: props.eventKey,
|
|
122
124
|
order,
|
|
123
125
|
product: props.product,
|
|
124
|
-
|
|
126
|
+
offering: props.offering,
|
|
127
|
+
enrollment: props.enrollment,
|
|
125
128
|
props,
|
|
126
129
|
billingAddress,
|
|
127
130
|
setBillingAddress,
|
|
@@ -8,6 +8,7 @@ import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
|
|
|
8
8
|
import { usePaymentSchedule } from 'hooks/usePaymentSchedule';
|
|
9
9
|
import { Spinner } from 'components/Spinner';
|
|
10
10
|
import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
|
|
11
|
+
import { ProductType } from 'types/Joanie';
|
|
11
12
|
|
|
12
13
|
const messages = defineMessages({
|
|
13
14
|
title: {
|
|
@@ -54,6 +55,7 @@ const messages = defineMessages({
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
export const SaleTunnelInformation = () => {
|
|
58
|
+
const { product } = useSaleTunnelContext();
|
|
57
59
|
return (
|
|
58
60
|
<div className="sale-tunnel__main__column sale-tunnel__information">
|
|
59
61
|
<div>
|
|
@@ -70,7 +72,7 @@ export const SaleTunnelInformation = () => {
|
|
|
70
72
|
</div>
|
|
71
73
|
</div>
|
|
72
74
|
<div>
|
|
73
|
-
<PaymentScheduleBlock />
|
|
75
|
+
{product.type === ProductType.CREDENTIAL && <PaymentScheduleBlock />}
|
|
74
76
|
<Total />
|
|
75
77
|
<WithdrawRightCheckbox />
|
|
76
78
|
</div>
|
|
@@ -99,7 +101,11 @@ const Email = () => {
|
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const Total = () => {
|
|
102
|
-
const { product,
|
|
104
|
+
const { product, offering, enrollment } = useSaleTunnelContext();
|
|
105
|
+
const totalPrice =
|
|
106
|
+
enrollment?.offerings?.[0]?.rules?.discounted_price ??
|
|
107
|
+
offering?.rules?.discounted_price ??
|
|
108
|
+
product.price;
|
|
103
109
|
return (
|
|
104
110
|
<div className="sale-tunnel__total">
|
|
105
111
|
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
|
|
@@ -107,11 +113,7 @@ const Total = () => {
|
|
|
107
113
|
<FormattedMessage {...messages.totalLabel} />
|
|
108
114
|
</div>
|
|
109
115
|
<div className="block-title">
|
|
110
|
-
<FormattedNumber
|
|
111
|
-
value={relation?.discounted_price || product.price}
|
|
112
|
-
style="currency"
|
|
113
|
-
currency={product.price_currency}
|
|
114
|
-
/>
|
|
116
|
+
<FormattedNumber value={totalPrice} style="currency" currency={product.price_currency} />
|
|
115
117
|
</div>
|
|
116
118
|
</div>
|
|
117
119
|
</div>
|
|
@@ -15,7 +15,7 @@ import CourseProductItem from 'widgets/SyllabusCourseRunsList/components/CourseP
|
|
|
15
15
|
import {
|
|
16
16
|
AddressFactory,
|
|
17
17
|
ContractFactory,
|
|
18
|
-
|
|
18
|
+
OfferingFactory,
|
|
19
19
|
CredentialOrderFactory,
|
|
20
20
|
CreditCardFactory,
|
|
21
21
|
PaymentFactory,
|
|
@@ -99,7 +99,7 @@ describe('SaleTunnel', () => {
|
|
|
99
99
|
*/
|
|
100
100
|
const course = PacedCourseFactory().one();
|
|
101
101
|
const product = ProductFactory().one();
|
|
102
|
-
const
|
|
102
|
+
const offering = OfferingFactory({
|
|
103
103
|
course,
|
|
104
104
|
product,
|
|
105
105
|
is_withdrawable: false,
|
|
@@ -108,7 +108,7 @@ describe('SaleTunnel', () => {
|
|
|
108
108
|
|
|
109
109
|
fetchMock.get(
|
|
110
110
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
|
|
111
|
-
|
|
111
|
+
offering,
|
|
112
112
|
);
|
|
113
113
|
fetchMock.get(
|
|
114
114
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|