richie-education 3.1.3-dev11 → 3.1.3-dev15
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 +11 -20
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +22 -35
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -10
- 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 -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/useOffer/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 +14 -17
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
- 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 -23
- 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 -6
- 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 -7
- 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 +25 -33
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
- package/js/types/Joanie.ts +25 -22
- 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 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
- 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/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -18
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +81 -99
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -31
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
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
|
+
offers: {
|
|
131
|
+
get: `${baseUrl}/organizations/:organization_id/offers/: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
|
+
offers: {
|
|
160
|
+
get: `${baseUrl}/offers/: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
|
+
offers: {
|
|
474
|
+
get: (filters?: Joanie.OfferQueryFilters) => {
|
|
475
475
|
return fetchWithJWT(
|
|
476
476
|
filters?.organization_id
|
|
477
|
-
? buildApiUrl(ROUTES.organizations.
|
|
478
|
-
: buildApiUrl(ROUTES.
|
|
477
|
+
? buildApiUrl(ROUTES.organizations.offers.get, filters)
|
|
478
|
+
: buildApiUrl(ROUTES.offers.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, OfferFactory, 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 { isOffer } from 'types/Joanie';
|
|
17
13
|
import { Props } from './AbstractContractFrame';
|
|
18
14
|
import { OrganizationContractFrame } from '.';
|
|
19
15
|
|
|
@@ -77,31 +73,28 @@ describe('OrganizationContractFrame', () => {
|
|
|
77
73
|
|
|
78
74
|
it.each([
|
|
79
75
|
{
|
|
80
|
-
label: 'contractList: undefined,
|
|
76
|
+
label: 'contractList: undefined, offer: undefined',
|
|
81
77
|
contractList: undefined,
|
|
82
|
-
|
|
78
|
+
offer: undefined,
|
|
83
79
|
},
|
|
84
80
|
{
|
|
85
|
-
label: 'contractList: 2 Contract,
|
|
81
|
+
label: 'contractList: 2 Contract, offer: undefined',
|
|
86
82
|
contractList: ContractFactory().many(2),
|
|
87
|
-
|
|
83
|
+
offer: undefined,
|
|
88
84
|
},
|
|
89
85
|
{
|
|
90
|
-
label: 'contractList: undefined,
|
|
86
|
+
label: 'contractList: undefined, offer: one Offer',
|
|
91
87
|
contractList: undefined,
|
|
92
|
-
|
|
88
|
+
offer: OfferFactory().one(),
|
|
93
89
|
},
|
|
94
90
|
])(
|
|
95
91
|
'should implement AbstractContractFrame for organization and $label',
|
|
96
|
-
async ({ contractList,
|
|
92
|
+
async ({ contractList, offer }) => {
|
|
97
93
|
const organization = OrganizationFactory().one();
|
|
98
94
|
const contracts = contractList || ContractFactory().many(2);
|
|
99
95
|
const isOpen = faker.datatype.boolean();
|
|
100
96
|
|
|
101
|
-
const invitationLinkQueryString =
|
|
102
|
-
courseProductRelation && isCourseProductRelation(courseProductRelation)
|
|
103
|
-
? `?course_product_relation_ids=${courseProductRelation.id}`
|
|
104
|
-
: '';
|
|
97
|
+
const invitationLinkQueryString = offer && isOffer(offer) ? `?offer_ids=${offer.id}` : '';
|
|
105
98
|
const expectedUrls = {
|
|
106
99
|
getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
|
|
107
100
|
checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
|
|
@@ -133,9 +126,7 @@ describe('OrganizationContractFrame', () => {
|
|
|
133
126
|
<Wrapper client={client}>
|
|
134
127
|
<OrganizationContractFrame
|
|
135
128
|
organizationId={organization.id}
|
|
136
|
-
|
|
137
|
-
courseProductRelation ? [courseProductRelation.id] : undefined
|
|
138
|
-
}
|
|
129
|
+
offerIds={offer ? [offer.id] : undefined}
|
|
139
130
|
isOpen={isOpen}
|
|
140
131
|
onDone={handleDone}
|
|
141
132
|
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, Offer } from 'types/Joanie';
|
|
8
8
|
|
|
9
9
|
interface Props extends AbstractProps {
|
|
10
10
|
contractIds?: Contract['id'][];
|
|
11
11
|
organizationId: string;
|
|
12
|
-
|
|
12
|
+
offerIds?: Offer['id'][];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const OrganizationContractFrame = ({
|
|
16
16
|
organizationId,
|
|
17
|
-
|
|
17
|
+
offerIds = [],
|
|
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
|
+
offer_ids: offerIds,
|
|
33
33
|
organization_id: organizationId,
|
|
34
34
|
contracts_ids: contractIds,
|
|
35
35
|
});
|
|
@@ -6,23 +6,18 @@ import {
|
|
|
6
6
|
Course as RichieCourse,
|
|
7
7
|
isRichieCourse,
|
|
8
8
|
} from 'types/Course';
|
|
9
|
-
import {
|
|
10
|
-
CourseListItem as JoanieCourse,
|
|
11
|
-
CourseProductRelationLight,
|
|
12
|
-
isCourseProductRelation,
|
|
13
|
-
ProductType,
|
|
14
|
-
} from 'types/Joanie';
|
|
9
|
+
import { CourseListItem as JoanieCourse, OfferLight, isOffer, ProductType } from 'types/Joanie';
|
|
15
10
|
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
|
|
16
11
|
import { CourseGlimpseCourse } from '.';
|
|
17
12
|
|
|
18
|
-
const
|
|
19
|
-
|
|
13
|
+
const getCourseGlimpsePropsFromOffer = (
|
|
14
|
+
offer: OfferLight,
|
|
20
15
|
intl: IntlShape,
|
|
21
16
|
organizationId?: string,
|
|
22
17
|
): CourseGlimpseCourse => {
|
|
23
18
|
const courseRouteParams = {
|
|
24
|
-
courseId:
|
|
25
|
-
|
|
19
|
+
courseId: offer.course.id,
|
|
20
|
+
offerId: offer.id,
|
|
26
21
|
};
|
|
27
22
|
const courseRoute = organizationId
|
|
28
23
|
? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
|
|
@@ -31,35 +26,27 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
|
|
|
31
26
|
})
|
|
32
27
|
: generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
|
|
33
28
|
return {
|
|
34
|
-
id:
|
|
35
|
-
code:
|
|
36
|
-
title:
|
|
37
|
-
cover_image:
|
|
29
|
+
id: offer.id,
|
|
30
|
+
code: offer.course.code,
|
|
31
|
+
title: offer.product.title,
|
|
32
|
+
cover_image: offer.course.cover
|
|
38
33
|
? {
|
|
39
|
-
src:
|
|
34
|
+
src: offer.course.cover.src,
|
|
40
35
|
}
|
|
41
36
|
: null,
|
|
42
37
|
organization: {
|
|
43
|
-
title:
|
|
44
|
-
image:
|
|
38
|
+
title: offer.organizations[0].title,
|
|
39
|
+
image: offer.organizations[0].logo || null,
|
|
45
40
|
},
|
|
46
|
-
product_id:
|
|
41
|
+
product_id: offer.product.id,
|
|
47
42
|
course_route: courseRoute,
|
|
48
|
-
state:
|
|
43
|
+
state: offer.product.state,
|
|
49
44
|
certificate_offer:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
courseProductRelation.product.type === ProductType.CERTIFICATE
|
|
56
|
-
? courseProductRelation.product.price
|
|
57
|
-
: null,
|
|
58
|
-
price:
|
|
59
|
-
courseProductRelation.product.type === ProductType.CREDENTIAL
|
|
60
|
-
? courseProductRelation.product.price
|
|
61
|
-
: null,
|
|
62
|
-
price_currency: courseProductRelation.product.price_currency,
|
|
45
|
+
offer.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
|
|
46
|
+
offer: offer.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
|
|
47
|
+
certificate_price: offer.product.type === ProductType.CERTIFICATE ? offer.product.price : null,
|
|
48
|
+
price: offer.product.type === ProductType.CREDENTIAL ? offer.product.price : null,
|
|
49
|
+
price_currency: offer.product.price_currency,
|
|
63
50
|
};
|
|
64
51
|
};
|
|
65
52
|
|
|
@@ -125,12 +112,12 @@ const getCourseGlimpsePropsFromJoanieCourse = (
|
|
|
125
112
|
};
|
|
126
113
|
|
|
127
114
|
export const getCourseGlimpseProps = (
|
|
128
|
-
course: RichieCourse | (JoanieCourse |
|
|
115
|
+
course: RichieCourse | (JoanieCourse | OfferLight),
|
|
129
116
|
intl?: IntlShape,
|
|
130
117
|
organizationId?: string,
|
|
131
118
|
): CourseGlimpseCourse => {
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
119
|
+
if (isOffer(course)) {
|
|
120
|
+
return getCourseGlimpsePropsFromOffer(course, intl!, organizationId);
|
|
134
121
|
}
|
|
135
122
|
|
|
136
123
|
if (isRichieCourse(course)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IntlShape } from 'react-intl';
|
|
2
|
-
import {
|
|
2
|
+
import { OfferLight, 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 | OfferLight)[],
|
|
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
|
+
offer?: Joanie.Offer;
|
|
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
|
+
offer,
|
|
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
|
+
offer={offer}
|
|
144
144
|
enrollment={enrollment}
|
|
145
145
|
course={course}
|
|
146
146
|
isWithdrawable={isWithdrawable}
|
|
@@ -10,14 +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 {
|
|
14
|
-
Address,
|
|
15
|
-
CourseProductRelation,
|
|
16
|
-
CreditCard,
|
|
17
|
-
Order,
|
|
18
|
-
OrderState,
|
|
19
|
-
Product,
|
|
20
|
-
} from 'types/Joanie';
|
|
13
|
+
import { Address, Offer, CreditCard, Order, OrderState, Product } from 'types/Joanie';
|
|
21
14
|
import useProductOrder from 'hooks/useProductOrder';
|
|
22
15
|
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
|
|
23
16
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
@@ -33,7 +26,7 @@ export interface SaleTunnelContextType {
|
|
|
33
26
|
order?: Order;
|
|
34
27
|
product: Product;
|
|
35
28
|
webAnalyticsEventKey: string;
|
|
36
|
-
|
|
29
|
+
offer?: Offer;
|
|
37
30
|
|
|
38
31
|
// internal
|
|
39
32
|
step: SaleTunnelStep;
|
|
@@ -121,7 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
121
114
|
webAnalyticsEventKey: props.eventKey,
|
|
122
115
|
order,
|
|
123
116
|
product: props.product,
|
|
124
|
-
|
|
117
|
+
offer: props.offer,
|
|
125
118
|
props,
|
|
126
119
|
billingAddress,
|
|
127
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, offer } = 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={offer?.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
|
+
OfferFactory,
|
|
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 offer = OfferFactory({
|
|
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
|
+
offer,
|
|
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
|
+
OfferFactory,
|
|
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 offer = OfferFactory({
|
|
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 } = offer;
|
|
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} offer={offer} 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(offer!.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
|
+
Offer,
|
|
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
|
+
offer?: Offer;
|
|
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, OfferFactory } 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 = OfferFactory().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 = OfferFactory().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, OfferLight } 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 | OfferLight)[];
|
|
35
35
|
isLoadingMore?: boolean;
|
|
36
36
|
hasMore?: boolean;
|
|
37
37
|
isNewSearchLoading?: boolean;
|