richie-education 3.1.3-dev12 → 3.1.3-dev17
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/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +12 -11
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +28 -22
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -3
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +5 -3
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +76 -63
- 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 -16
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -4
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -4
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +17 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -8
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +6 -3
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -10
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -8
- 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 -21
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +19 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +6 -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 +7 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -5
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +55 -55
- 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 +25 -25
- package/js/pages/TeacherDashboardTraining/index.tsx +16 -12
- package/js/types/Joanie.ts +27 -20
- package/js/utils/test/factories/joanie.ts +14 -11
- 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 -23
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +20 -17
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +22 -16
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -3
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +14 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +105 -75
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +27 -20
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useOffer/index.ts +0 -32
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,10 +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 { ContractFactory,
|
|
9
|
+
import { ContractFactory, OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
10
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
11
11
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
12
|
-
import {
|
|
12
|
+
import { isOffering } from 'types/Joanie';
|
|
13
13
|
import { Props } from './AbstractContractFrame';
|
|
14
14
|
import { OrganizationContractFrame } from '.';
|
|
15
15
|
|
|
@@ -73,28 +73,29 @@ describe('OrganizationContractFrame', () => {
|
|
|
73
73
|
|
|
74
74
|
it.each([
|
|
75
75
|
{
|
|
76
|
-
label: 'contractList: undefined,
|
|
76
|
+
label: 'contractList: undefined, offering: undefined',
|
|
77
77
|
contractList: undefined,
|
|
78
|
-
|
|
78
|
+
offering: undefined,
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
|
-
label: 'contractList: 2 Contract,
|
|
81
|
+
label: 'contractList: 2 Contract, offering: undefined',
|
|
82
82
|
contractList: ContractFactory().many(2),
|
|
83
|
-
|
|
83
|
+
offering: undefined,
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
|
-
label: 'contractList: undefined,
|
|
86
|
+
label: 'contractList: undefined, offering: one Offering',
|
|
87
87
|
contractList: undefined,
|
|
88
|
-
|
|
88
|
+
offering: OfferingFactory().one(),
|
|
89
89
|
},
|
|
90
90
|
])(
|
|
91
91
|
'should implement AbstractContractFrame for organization and $label',
|
|
92
|
-
async ({ contractList,
|
|
92
|
+
async ({ contractList, offering }) => {
|
|
93
93
|
const organization = OrganizationFactory().one();
|
|
94
94
|
const contracts = contractList || ContractFactory().many(2);
|
|
95
95
|
const isOpen = faker.datatype.boolean();
|
|
96
96
|
|
|
97
|
-
const invitationLinkQueryString =
|
|
97
|
+
const invitationLinkQueryString =
|
|
98
|
+
offering && isOffering(offering) ? `?offering_ids=${offering.id}` : '';
|
|
98
99
|
const expectedUrls = {
|
|
99
100
|
getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
|
|
100
101
|
checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
|
|
@@ -126,7 +127,7 @@ describe('OrganizationContractFrame', () => {
|
|
|
126
127
|
<Wrapper client={client}>
|
|
127
128
|
<OrganizationContractFrame
|
|
128
129
|
organizationId={organization.id}
|
|
129
|
-
|
|
130
|
+
offeringIds={offering ? [offering.id] : undefined}
|
|
130
131
|
isOpen={isOpen}
|
|
131
132
|
onDone={handleDone}
|
|
132
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
|
});
|
|
@@ -6,18 +6,23 @@ import {
|
|
|
6
6
|
Course as RichieCourse,
|
|
7
7
|
isRichieCourse,
|
|
8
8
|
} from 'types/Course';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CourseListItem as JoanieCourse,
|
|
11
|
+
OfferingLight,
|
|
12
|
+
isOffering,
|
|
13
|
+
ProductType,
|
|
14
|
+
} from 'types/Joanie';
|
|
10
15
|
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
|
|
11
16
|
import { CourseGlimpseCourse } from '.';
|
|
12
17
|
|
|
13
|
-
const
|
|
14
|
-
|
|
18
|
+
const getCourseGlimpsePropsFromOffering = (
|
|
19
|
+
offering: OfferingLight,
|
|
15
20
|
intl: IntlShape,
|
|
16
21
|
organizationId?: string,
|
|
17
22
|
): CourseGlimpseCourse => {
|
|
18
23
|
const courseRouteParams = {
|
|
19
|
-
courseId:
|
|
20
|
-
|
|
24
|
+
courseId: offering.course.id,
|
|
25
|
+
offeringId: offering.id,
|
|
21
26
|
};
|
|
22
27
|
const courseRoute = organizationId
|
|
23
28
|
? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
|
|
@@ -26,27 +31,28 @@ const getCourseGlimpsePropsFromOffer = (
|
|
|
26
31
|
})
|
|
27
32
|
: generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
|
|
28
33
|
return {
|
|
29
|
-
id:
|
|
30
|
-
code:
|
|
31
|
-
title:
|
|
32
|
-
cover_image:
|
|
34
|
+
id: offering.id,
|
|
35
|
+
code: offering.course.code,
|
|
36
|
+
title: offering.product.title,
|
|
37
|
+
cover_image: offering.course.cover
|
|
33
38
|
? {
|
|
34
|
-
src:
|
|
39
|
+
src: offering.course.cover.src,
|
|
35
40
|
}
|
|
36
41
|
: null,
|
|
37
42
|
organization: {
|
|
38
|
-
title:
|
|
39
|
-
image:
|
|
43
|
+
title: offering.organizations[0].title,
|
|
44
|
+
image: offering.organizations[0].logo || null,
|
|
40
45
|
},
|
|
41
|
-
product_id:
|
|
46
|
+
product_id: offering.product.id,
|
|
42
47
|
course_route: courseRoute,
|
|
43
|
-
state:
|
|
48
|
+
state: offering.product.state,
|
|
44
49
|
certificate_offer:
|
|
45
|
-
|
|
46
|
-
offer:
|
|
47
|
-
certificate_price:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
offering.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
|
|
51
|
+
offer: offering.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
|
|
52
|
+
certificate_price:
|
|
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,
|
|
50
56
|
};
|
|
51
57
|
};
|
|
52
58
|
|
|
@@ -112,12 +118,12 @@ const getCourseGlimpsePropsFromJoanieCourse = (
|
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
export const getCourseGlimpseProps = (
|
|
115
|
-
course: RichieCourse | (JoanieCourse |
|
|
121
|
+
course: RichieCourse | (JoanieCourse | OfferingLight),
|
|
116
122
|
intl?: IntlShape,
|
|
117
123
|
organizationId?: string,
|
|
118
124
|
): CourseGlimpseCourse => {
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
125
|
+
if (isOffering(course)) {
|
|
126
|
+
return getCourseGlimpsePropsFromOffering(course, intl!, organizationId);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
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}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
|
|
12
12
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
13
|
-
import { Address,
|
|
13
|
+
import { Address, Offering, CreditCard, Order, OrderState, Product } from 'types/Joanie';
|
|
14
14
|
import useProductOrder from 'hooks/useProductOrder';
|
|
15
15
|
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
|
|
16
16
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
@@ -26,7 +26,7 @@ export interface SaleTunnelContextType {
|
|
|
26
26
|
order?: Order;
|
|
27
27
|
product: Product;
|
|
28
28
|
webAnalyticsEventKey: string;
|
|
29
|
-
|
|
29
|
+
offering?: Offering;
|
|
30
30
|
|
|
31
31
|
// internal
|
|
32
32
|
step: SaleTunnelStep;
|
|
@@ -114,7 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
114
114
|
webAnalyticsEventKey: props.eventKey,
|
|
115
115
|
order,
|
|
116
116
|
product: props.product,
|
|
117
|
-
|
|
117
|
+
offering: props.offering,
|
|
118
118
|
props,
|
|
119
119
|
billingAddress,
|
|
120
120
|
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,7 @@ const Email = () => {
|
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const Total = () => {
|
|
102
|
-
const { product,
|
|
104
|
+
const { product, offering } = useSaleTunnelContext();
|
|
103
105
|
return (
|
|
104
106
|
<div className="sale-tunnel__total">
|
|
105
107
|
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
|
|
@@ -108,7 +110,7 @@ const Total = () => {
|
|
|
108
110
|
</div>
|
|
109
111
|
<div className="block-title">
|
|
110
112
|
<FormattedNumber
|
|
111
|
-
value={
|
|
113
|
+
value={offering?.rules.discounted_price || product.price}
|
|
112
114
|
style="currency"
|
|
113
115
|
currency={product.price_currency}
|
|
114
116
|
/>
|
|
@@ -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/`,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
AddressFactory,
|
|
16
16
|
CertificateOrderFactory,
|
|
17
17
|
CertificateProductFactory,
|
|
18
|
-
|
|
18
|
+
OfferingFactory,
|
|
19
19
|
CredentialOrderFactory,
|
|
20
20
|
CredentialProductFactory,
|
|
21
21
|
CreditCardFactory,
|
|
@@ -182,7 +182,9 @@ describe.each([
|
|
|
182
182
|
nbApiCalls += 1; // useProductOrder call.
|
|
183
183
|
nbApiCalls += 1; // get user account call.
|
|
184
184
|
nbApiCalls += 1; // get user preferences call.
|
|
185
|
-
|
|
185
|
+
if (product.type === ProductType.CREDENTIAL) {
|
|
186
|
+
nbApiCalls += 1; // product payment-schedule call
|
|
187
|
+
}
|
|
186
188
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
187
189
|
|
|
188
190
|
const user = userEvent.setup({ delay: null });
|
|
@@ -269,7 +271,9 @@ describe.each([
|
|
|
269
271
|
nbApiCalls += 1; // useProductOrder get order with filters
|
|
270
272
|
nbApiCalls += 1; // get user account call.
|
|
271
273
|
nbApiCalls += 1; // get user preferences call.
|
|
272
|
-
|
|
274
|
+
if (product.type === ProductType.CREDENTIAL) {
|
|
275
|
+
nbApiCalls += 1; // get product payment schedule.
|
|
276
|
+
}
|
|
273
277
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
274
278
|
|
|
275
279
|
const user = userEvent.setup({ delay: null });
|
|
@@ -403,36 +407,41 @@ describe.each([
|
|
|
403
407
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
404
408
|
});
|
|
405
409
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
if (product.type === ProductType.CREDENTIAL) {
|
|
411
|
+
await screen.findByRole('heading', {
|
|
412
|
+
level: 4,
|
|
413
|
+
name: 'Payment schedule',
|
|
414
|
+
});
|
|
410
415
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
416
|
+
const scheduleTable = screen.getByRole('table');
|
|
417
|
+
const scheduleTableRows = within(scheduleTable).getAllByRole('row');
|
|
418
|
+
expect(scheduleTableRows).toHaveLength(schedule.length);
|
|
414
419
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
420
|
+
scheduleTableRows.forEach((row, index) => {
|
|
421
|
+
const installment = schedule[index];
|
|
422
|
+
// A first column should show the installment index
|
|
423
|
+
within(row).getByRole('cell', {
|
|
424
|
+
name: (index + 1).toString(),
|
|
425
|
+
});
|
|
426
|
+
// A 2nd column should show the installment amount
|
|
427
|
+
within(row).getByRole('cell', {
|
|
428
|
+
name: formatPrice(installment.amount, installment.currency),
|
|
429
|
+
});
|
|
430
|
+
// A 3rd column should show the installment withdraw date
|
|
431
|
+
within(row).getByRole('cell', {
|
|
432
|
+
name: `Withdrawn on ${intl.formatDate(installment.due_date, {
|
|
433
|
+
...DEFAULT_DATE_FORMAT,
|
|
434
|
+
})}`,
|
|
435
|
+
});
|
|
436
|
+
// A 4th column should show the installment state
|
|
437
|
+
within(row).getByRole('cell', {
|
|
438
|
+
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
439
|
+
});
|
|
434
440
|
});
|
|
435
|
-
}
|
|
441
|
+
} else {
|
|
442
|
+
expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
|
|
443
|
+
expect(screen.queryByRole('table')).toBeNull();
|
|
444
|
+
}
|
|
436
445
|
|
|
437
446
|
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
438
447
|
expect($totalAmount).toHaveTextContent(
|
|
@@ -444,15 +453,17 @@ describe.each([
|
|
|
444
453
|
const intl = createIntl({ locale: 'en' });
|
|
445
454
|
const schedule = PaymentInstallmentFactory().many(2);
|
|
446
455
|
|
|
447
|
-
const
|
|
456
|
+
const offering = OfferingFactory({
|
|
448
457
|
product: ProductFactory({
|
|
449
458
|
price: 840,
|
|
450
459
|
price_currency: 'EUR',
|
|
451
460
|
}).one(),
|
|
452
|
-
|
|
453
|
-
|
|
461
|
+
rules: {
|
|
462
|
+
discounted_price: 800,
|
|
463
|
+
discount_rate: 0.3,
|
|
464
|
+
},
|
|
454
465
|
}).one();
|
|
455
|
-
const { product } =
|
|
466
|
+
const { product } = offering;
|
|
456
467
|
|
|
457
468
|
fetchMock
|
|
458
469
|
.get(
|
|
@@ -464,45 +475,47 @@ describe.each([
|
|
|
464
475
|
schedule,
|
|
465
476
|
);
|
|
466
477
|
|
|
467
|
-
render(<Wrapper product={product}
|
|
478
|
+
render(<Wrapper product={product} offering={offering} isWithdrawable={true} />, {
|
|
468
479
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
469
480
|
});
|
|
470
481
|
|
|
471
|
-
|
|
472
|
-
level: 4,
|
|
473
|
-
name: 'Payment schedule',
|
|
474
|
-
});
|
|
482
|
+
if (product.type === ProductType.CREDENTIAL) {
|
|
483
|
+
await screen.findByRole('heading', { level: 4, name: 'Payment schedule' });
|
|
475
484
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
485
|
+
const scheduleTable = screen.getByRole('table');
|
|
486
|
+
const scheduleTableRows = within(scheduleTable).getAllByRole('row');
|
|
487
|
+
expect(scheduleTableRows).toHaveLength(schedule.length);
|
|
479
488
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
489
|
+
scheduleTableRows.forEach((row, index) => {
|
|
490
|
+
const installment = schedule[index];
|
|
491
|
+
// A first column should show the installment index
|
|
492
|
+
within(row).getByRole('cell', {
|
|
493
|
+
name: (index + 1).toString(),
|
|
494
|
+
});
|
|
495
|
+
// A 2nd column should show the installment amount
|
|
496
|
+
within(row).getByRole('cell', {
|
|
497
|
+
name: formatPrice(installment.amount, installment.currency),
|
|
498
|
+
});
|
|
499
|
+
// A 3rd column should show the installment withdraw date
|
|
500
|
+
within(row).getByRole('cell', {
|
|
501
|
+
name: `Withdrawn on ${intl.formatDate(installment.due_date, {
|
|
502
|
+
...DEFAULT_DATE_FORMAT,
|
|
503
|
+
})}`,
|
|
504
|
+
});
|
|
505
|
+
// A 4th column should show the installment state
|
|
506
|
+
within(row).getByRole('cell', {
|
|
507
|
+
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
508
|
+
});
|
|
499
509
|
});
|
|
500
|
-
}
|
|
510
|
+
} else {
|
|
511
|
+
expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
|
|
512
|
+
expect(screen.queryByRole('table')).toBeNull();
|
|
513
|
+
}
|
|
501
514
|
|
|
502
515
|
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
503
516
|
expect($totalAmount).toHaveTextContent(
|
|
504
517
|
'Total' +
|
|
505
|
-
formatPrice(
|
|
518
|
+
formatPrice(offering!.rules.discounted_price!, product.price_currency).replace(
|
|
506
519
|
/(\u202F|\u00a0)/g,
|
|
507
520
|
' ',
|
|
508
521
|
),
|
|
@@ -2,7 +2,7 @@ import { ModalProps } from '@openfun/cunningham-react';
|
|
|
2
2
|
import {
|
|
3
3
|
CertificateProduct,
|
|
4
4
|
CourseLight,
|
|
5
|
-
|
|
5
|
+
Offering,
|
|
6
6
|
CredentialProduct,
|
|
7
7
|
Enrollment,
|
|
8
8
|
Order,
|
|
@@ -16,7 +16,7 @@ import { PacedCourse } from 'types';
|
|
|
16
16
|
|
|
17
17
|
export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
|
|
18
18
|
product: Product;
|
|
19
|
-
|
|
19
|
+
offering?: Offering;
|
|
20
20
|
organizations?: Organization[];
|
|
21
21
|
isWithdrawable: boolean;
|
|
22
22
|
course?: PacedCourse | CourseLight;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { screen } from '@testing-library/react';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
3
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
4
|
-
import { CourseListItemFactory,
|
|
4
|
+
import { CourseListItemFactory, OfferingFactory } from 'utils/test/factories/joanie';
|
|
5
5
|
import { render } from 'utils/test/render';
|
|
6
6
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
7
7
|
import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
|
|
@@ -35,7 +35,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('should render loading more state', async () => {
|
|
38
|
-
const trainings =
|
|
38
|
+
const trainings = OfferingFactory().many(2);
|
|
39
39
|
const courses = CourseListItemFactory().many(2);
|
|
40
40
|
const courseAndProductList = [...courses, ...trainings];
|
|
41
41
|
|
|
@@ -60,7 +60,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('should render courses and products list', async () => {
|
|
63
|
-
const trainings =
|
|
63
|
+
const trainings = OfferingFactory().many(2);
|
|
64
64
|
const courses = CourseListItemFactory().many(2);
|
|
65
65
|
const courseAndProductList = [...courses, ...trainings];
|
|
66
66
|
|
|
@@ -6,7 +6,7 @@ import { CourseGlimpseList, getCourseGlimpseListProps } from 'components/CourseG
|
|
|
6
6
|
import { Spinner } from 'components/Spinner';
|
|
7
7
|
import context from 'utils/context';
|
|
8
8
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
|
9
|
-
import { CourseListItem,
|
|
9
|
+
import { CourseListItem, OfferingLight } from 'types/Joanie';
|
|
10
10
|
import Banner from 'components/Banner';
|
|
11
11
|
|
|
12
12
|
const messages = defineMessages({
|
|
@@ -31,7 +31,7 @@ interface TeacherDashboardCourseListProps {
|
|
|
31
31
|
titleTranslated?: string;
|
|
32
32
|
organizationId?: string;
|
|
33
33
|
loadMore: () => void;
|
|
34
|
-
courseAndProductList?: (CourseListItem |
|
|
34
|
+
courseAndProductList?: (CourseListItem | OfferingLight)[];
|
|
35
35
|
isLoadingMore?: boolean;
|
|
36
36
|
hasMore?: boolean;
|
|
37
37
|
isNewSearchLoading?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
2
|
-
import {
|
|
2
|
+
import { Offering, Organization } from 'types/Joanie';
|
|
3
3
|
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
4
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
5
5
|
import { handle } from 'utils/errors/handle';
|
|
@@ -53,11 +53,11 @@ const useContractArchive = () => {
|
|
|
53
53
|
},
|
|
54
54
|
create: async (
|
|
55
55
|
organizationId?: Organization['id'],
|
|
56
|
-
|
|
56
|
+
offeringId?: Offering['id'],
|
|
57
57
|
): Promise<string> => {
|
|
58
58
|
const response = await api.user.contracts.zip_archive.create({
|
|
59
59
|
organization_id: organizationId,
|
|
60
|
-
|
|
60
|
+
offering_id: offeringId,
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
return extractArchiveId(response.url);
|