richie-education 3.1.2 → 3.1.3-dev12
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/CredentialSaleTunnel/index.tsx +1 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
- package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +75 -0
- package/js/components/SaleTunnel/index.stories.tsx +0 -1
- 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 +26 -30
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +12 -25
- 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 +23 -38
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +27 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +174 -158
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +9 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +108 -28
- 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,8 +66,8 @@ interface CertificatePurchaseButtonProps extends PurchaseButtonPropsBase {
|
|
|
66
66
|
const PurchaseButton = ({
|
|
67
67
|
product,
|
|
68
68
|
course,
|
|
69
|
+
offer,
|
|
69
70
|
enrollment,
|
|
70
|
-
orderGroup,
|
|
71
71
|
isWithdrawable,
|
|
72
72
|
organizations,
|
|
73
73
|
disabled = false,
|
|
@@ -140,8 +140,8 @@ const PurchaseButton = ({
|
|
|
140
140
|
{...saleTunnelModal}
|
|
141
141
|
product={product}
|
|
142
142
|
organizations={organizations}
|
|
143
|
+
offer={offer}
|
|
143
144
|
enrollment={enrollment}
|
|
144
|
-
orderGroup={orderGroup}
|
|
145
145
|
course={course}
|
|
146
146
|
isWithdrawable={isWithdrawable}
|
|
147
147
|
onFinish={onFinish}
|
|
@@ -19,9 +19,7 @@ export const CredentialSaleTunnel = (props: CredentialSaleTunnelProps) => {
|
|
|
19
19
|
);
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const CredentialPaymentButton = ({
|
|
23
|
-
course,
|
|
24
|
-
}: Pick<CredentialSaleTunnelProps, 'course' | 'orderGroup'>) => {
|
|
22
|
+
const CredentialPaymentButton = ({ course }: Pick<CredentialSaleTunnelProps, 'course'>) => {
|
|
25
23
|
return (
|
|
26
24
|
<SubscriptionButton
|
|
27
25
|
buildOrderPayload={(payload) => ({
|
|
@@ -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, CreditCard, Order, OrderState, Product } from 'types/Joanie';
|
|
13
|
+
import { Address, Offer, 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,6 +26,7 @@ export interface SaleTunnelContextType {
|
|
|
26
26
|
order?: Order;
|
|
27
27
|
product: Product;
|
|
28
28
|
webAnalyticsEventKey: string;
|
|
29
|
+
offer?: Offer;
|
|
29
30
|
|
|
30
31
|
// internal
|
|
31
32
|
step: SaleTunnelStep;
|
|
@@ -113,6 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
113
114
|
webAnalyticsEventKey: props.eventKey,
|
|
114
115
|
order,
|
|
115
116
|
product: props.product,
|
|
117
|
+
offer: props.offer,
|
|
116
118
|
props,
|
|
117
119
|
billingAddress,
|
|
118
120
|
setBillingAddress,
|
|
@@ -99,7 +99,7 @@ const Email = () => {
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const Total = () => {
|
|
102
|
-
const { product } = useSaleTunnelContext();
|
|
102
|
+
const { product, offer } = useSaleTunnelContext();
|
|
103
103
|
return (
|
|
104
104
|
<div className="sale-tunnel__total">
|
|
105
105
|
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
|
|
@@ -108,7 +108,7 @@ const Total = () => {
|
|
|
108
108
|
</div>
|
|
109
109
|
<div className="block-title">
|
|
110
110
|
<FormattedNumber
|
|
111
|
-
value={product.price}
|
|
111
|
+
value={offer?.discounted_price || product.price}
|
|
112
112
|
style="currency"
|
|
113
113
|
currency={product.price_currency}
|
|
114
114
|
/>
|
|
@@ -72,7 +72,7 @@ interface Props {
|
|
|
72
72
|
buildOrderPayload: (
|
|
73
73
|
payload: Pick<
|
|
74
74
|
OrderCreationPayload,
|
|
75
|
-
'product_id' | 'billing_address' | '
|
|
75
|
+
'product_id' | 'billing_address' | 'has_waived_withdrawal_right'
|
|
76
76
|
>,
|
|
77
77
|
) => OrderCreationPayload;
|
|
78
78
|
}
|
|
@@ -124,7 +124,6 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
|
|
|
124
124
|
const payload = buildOrderPayload({
|
|
125
125
|
product_id: product.id,
|
|
126
126
|
billing_address: billingAddress!,
|
|
127
|
-
order_group_id: saleTunnelProps.orderGroup?.id,
|
|
128
127
|
has_waived_withdrawal_right: hasWaivedWithdrawalRight,
|
|
129
128
|
});
|
|
130
129
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fetchMock from 'fetch-mock';
|
|
2
|
-
import {
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
3
|
import queryString from 'query-string';
|
|
4
4
|
import {
|
|
5
5
|
RichieContextFactory as mockRichieContextFactory,
|
|
@@ -11,11 +11,9 @@ import {
|
|
|
11
11
|
AddressFactory,
|
|
12
12
|
CredentialOrderFactory,
|
|
13
13
|
CredentialProductFactory,
|
|
14
|
-
OrderGroupFactory,
|
|
15
14
|
} from 'utils/test/factories/joanie';
|
|
16
15
|
import type * as Joanie from 'types/Joanie';
|
|
17
|
-
import {
|
|
18
|
-
import { NOT_CANCELED_ORDER_STATES, OrderCredentialCreationPayload } from 'types/Joanie';
|
|
16
|
+
import { NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
|
|
19
17
|
import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
20
18
|
import { render } from 'utils/test/render';
|
|
21
19
|
import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
|
|
@@ -83,11 +81,9 @@ describe('SaleTunnel / Credential', () => {
|
|
|
83
81
|
it('should create an order with an order group', async () => {
|
|
84
82
|
const course = PacedCourseFactory().one();
|
|
85
83
|
const product = CredentialProductFactory().one();
|
|
86
|
-
const orderGroup = OrderGroupFactory().one();
|
|
87
84
|
const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
const order = CredentialOrderFactory({ order_group_id: orderGroup.id }).one();
|
|
86
|
+
const order = CredentialOrderFactory().one();
|
|
91
87
|
const orderQueryParameters = {
|
|
92
88
|
course_code: course.code,
|
|
93
89
|
product_id: product.id,
|
|
@@ -100,15 +96,12 @@ describe('SaleTunnel / Credential', () => {
|
|
|
100
96
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
101
97
|
[],
|
|
102
98
|
)
|
|
103
|
-
.post('https://joanie.endpoint/api/v1.0/orders/',
|
|
104
|
-
createOrderPayload = JSON.parse(body as any);
|
|
105
|
-
return order;
|
|
106
|
-
})
|
|
99
|
+
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
107
100
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
108
101
|
overwriteRoutes: true,
|
|
109
102
|
});
|
|
110
103
|
|
|
111
|
-
render(<Wrapper product={product} course={course}
|
|
104
|
+
render(<Wrapper product={product} course={course} />, {
|
|
112
105
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
113
106
|
});
|
|
114
107
|
|
|
@@ -121,12 +114,5 @@ describe('SaleTunnel / Credential', () => {
|
|
|
121
114
|
|
|
122
115
|
// - Payment button should not be disabled.
|
|
123
116
|
expect($button.disabled).toBe(false);
|
|
124
|
-
|
|
125
|
-
// - User clicks on pay button
|
|
126
|
-
await act(async () => {
|
|
127
|
-
fireEvent.click($button);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await waitFor(() => expect(createOrderPayload?.order_group_id).toEqual(orderGroup.id));
|
|
131
117
|
});
|
|
132
118
|
});
|
|
@@ -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,6 +15,7 @@ import {
|
|
|
15
15
|
AddressFactory,
|
|
16
16
|
CertificateOrderFactory,
|
|
17
17
|
CertificateProductFactory,
|
|
18
|
+
OfferFactory,
|
|
18
19
|
CredentialOrderFactory,
|
|
19
20
|
CredentialProductFactory,
|
|
20
21
|
CreditCardFactory,
|
|
@@ -432,6 +433,80 @@ describe.each([
|
|
|
432
433
|
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
433
434
|
});
|
|
434
435
|
});
|
|
436
|
+
|
|
437
|
+
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
438
|
+
expect($totalAmount).toHaveTextContent(
|
|
439
|
+
'Total' + formatPrice(product.price, product.price_currency).replace(/(\u202F|\u00a0)/g, ' '),
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should show the product payment schedule with discounted price', async () => {
|
|
444
|
+
const intl = createIntl({ locale: 'en' });
|
|
445
|
+
const schedule = PaymentInstallmentFactory().many(2);
|
|
446
|
+
|
|
447
|
+
const offer = OfferFactory({
|
|
448
|
+
product: ProductFactory({
|
|
449
|
+
price: 840,
|
|
450
|
+
price_currency: 'EUR',
|
|
451
|
+
}).one(),
|
|
452
|
+
discounted_price: 800,
|
|
453
|
+
discount_rate: 0.3,
|
|
454
|
+
}).one();
|
|
455
|
+
const { product } = offer;
|
|
456
|
+
|
|
457
|
+
fetchMock
|
|
458
|
+
.get(
|
|
459
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
460
|
+
[],
|
|
461
|
+
)
|
|
462
|
+
.get(
|
|
463
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
464
|
+
schedule,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
render(<Wrapper product={product} offer={offer} isWithdrawable={true} />, {
|
|
468
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await screen.findByRole('heading', {
|
|
472
|
+
level: 4,
|
|
473
|
+
name: 'Payment schedule',
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const scheduleTable = screen.getByRole('table');
|
|
477
|
+
const scheduleTableRows = within(scheduleTable).getAllByRole('row');
|
|
478
|
+
expect(scheduleTableRows).toHaveLength(schedule.length);
|
|
479
|
+
|
|
480
|
+
scheduleTableRows.forEach((row, index) => {
|
|
481
|
+
const installment = schedule[index];
|
|
482
|
+
// A first column should show the installment index
|
|
483
|
+
within(row).getByRole('cell', {
|
|
484
|
+
name: (index + 1).toString(),
|
|
485
|
+
});
|
|
486
|
+
// A 2nd column should show the installment amount
|
|
487
|
+
within(row).getByRole('cell', {
|
|
488
|
+
name: formatPrice(installment.amount, installment.currency),
|
|
489
|
+
});
|
|
490
|
+
// A 3rd column should show the installment withdraw date
|
|
491
|
+
within(row).getByRole('cell', {
|
|
492
|
+
name: `Withdrawn on ${intl.formatDate(installment.due_date, {
|
|
493
|
+
...DEFAULT_DATE_FORMAT,
|
|
494
|
+
})}`,
|
|
495
|
+
});
|
|
496
|
+
// A 4th column should show the installment state
|
|
497
|
+
within(row).getByRole('cell', {
|
|
498
|
+
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
503
|
+
expect($totalAmount).toHaveTextContent(
|
|
504
|
+
'Total' +
|
|
505
|
+
formatPrice(offer!.discounted_price!, product.price_currency).replace(
|
|
506
|
+
/(\u202F|\u00a0)/g,
|
|
507
|
+
' ',
|
|
508
|
+
),
|
|
509
|
+
);
|
|
435
510
|
});
|
|
436
511
|
|
|
437
512
|
it('should show a walkthrough to explain the subscription process', async () => {
|
|
@@ -2,10 +2,10 @@ import { ModalProps } from '@openfun/cunningham-react';
|
|
|
2
2
|
import {
|
|
3
3
|
CertificateProduct,
|
|
4
4
|
CourseLight,
|
|
5
|
+
Offer,
|
|
5
6
|
CredentialProduct,
|
|
6
7
|
Enrollment,
|
|
7
8
|
Order,
|
|
8
|
-
OrderGroup,
|
|
9
9
|
Organization,
|
|
10
10
|
Product,
|
|
11
11
|
ProductType,
|
|
@@ -16,11 +16,11 @@ import { PacedCourse } from 'types';
|
|
|
16
16
|
|
|
17
17
|
export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
|
|
18
18
|
product: Product;
|
|
19
|
+
offer?: Offer;
|
|
19
20
|
organizations?: Organization[];
|
|
20
21
|
isWithdrawable: boolean;
|
|
21
22
|
course?: PacedCourse | CourseLight;
|
|
22
23
|
enrollment?: Enrollment;
|
|
23
|
-
orderGroup?: OrderGroup;
|
|
24
24
|
onFinish?: (order: Order) => void;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
2
|
-
import {
|
|
2
|
+
import { Offer, 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
|
+
offerId?: Offer['id'],
|
|
57
57
|
): Promise<string> => {
|
|
58
58
|
const response = await api.user.contracts.zip_archive.create({
|
|
59
59
|
organization_id: organizationId,
|
|
60
|
-
|
|
60
|
+
offer_id: offerId,
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
return extractArchiveId(response.url);
|