richie-education 3.1.3-dev11 → 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/GenericSaleTunnel.tsx +3 -10
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +5 -5
- 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 +17 -19
- package/js/utils/test/factories/joanie.ts +3 -3
- 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 +9 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +63 -87
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -34
- 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,
|
|
@@ -99,7 +99,7 @@ const Email = () => {
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const Total = () => {
|
|
102
|
-
const { product,
|
|
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={
|
|
111
|
+
value={offer?.discounted_price || product.price}
|
|
112
112
|
style="currency"
|
|
113
113
|
currency={product.price_currency}
|
|
114
114
|
/>
|
|
@@ -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,
|
|
@@ -444,7 +444,7 @@ describe.each([
|
|
|
444
444
|
const intl = createIntl({ locale: 'en' });
|
|
445
445
|
const schedule = PaymentInstallmentFactory().many(2);
|
|
446
446
|
|
|
447
|
-
const
|
|
447
|
+
const offer = OfferFactory({
|
|
448
448
|
product: ProductFactory({
|
|
449
449
|
price: 840,
|
|
450
450
|
price_currency: 'EUR',
|
|
@@ -452,7 +452,7 @@ describe.each([
|
|
|
452
452
|
discounted_price: 800,
|
|
453
453
|
discount_rate: 0.3,
|
|
454
454
|
}).one();
|
|
455
|
-
const { product } =
|
|
455
|
+
const { product } = offer;
|
|
456
456
|
|
|
457
457
|
fetchMock
|
|
458
458
|
.get(
|
|
@@ -464,7 +464,7 @@ describe.each([
|
|
|
464
464
|
schedule,
|
|
465
465
|
);
|
|
466
466
|
|
|
467
|
-
render(<Wrapper product={product}
|
|
467
|
+
render(<Wrapper product={product} offer={offer} isWithdrawable={true} />, {
|
|
468
468
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
469
469
|
});
|
|
470
470
|
|
|
@@ -502,7 +502,7 @@ describe.each([
|
|
|
502
502
|
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
503
503
|
expect($totalAmount).toHaveTextContent(
|
|
504
504
|
'Total' +
|
|
505
|
-
formatPrice(
|
|
505
|
+
formatPrice(offer!.discounted_price!, product.price_currency).replace(
|
|
506
506
|
/(\u202F|\u00a0)/g,
|
|
507
507
|
' ',
|
|
508
508
|
),
|
|
@@ -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;
|
|
@@ -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);
|
|
@@ -2,13 +2,13 @@ import { renderHook, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { PropsWithChildren } from 'react';
|
|
5
|
-
import { CourseListItem,
|
|
5
|
+
import { CourseListItem, Offer } from 'types/Joanie';
|
|
6
6
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
7
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
8
8
|
import { SessionProvider } from 'contexts/SessionContext';
|
|
9
9
|
import { getRoutes } from 'api/joanie';
|
|
10
10
|
import { mockPaginatedResponse } from 'utils/test/mockPaginatedResponse';
|
|
11
|
-
import { CourseListItemFactory,
|
|
11
|
+
import { CourseListItemFactory, OfferFactory } from 'utils/test/factories/joanie';
|
|
12
12
|
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
13
13
|
import { useCourseProductUnion } from '.';
|
|
14
14
|
|
|
@@ -41,12 +41,12 @@ const renderUseCourseProductUnion = ({ organizationId }: { organizationId?: stri
|
|
|
41
41
|
|
|
42
42
|
describe('useCourseProductUnion', () => {
|
|
43
43
|
let courseList: CourseListItem[];
|
|
44
|
-
let
|
|
44
|
+
let offerList: Offer[];
|
|
45
45
|
let nbApiCalls: number;
|
|
46
46
|
|
|
47
47
|
beforeEach(() => {
|
|
48
48
|
courseList = CourseListItemFactory().many(6);
|
|
49
|
-
|
|
49
|
+
offerList = OfferFactory().many(6);
|
|
50
50
|
|
|
51
51
|
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', [], { overwriteRoutes: true });
|
|
52
52
|
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', [], { overwriteRoutes: true });
|
|
@@ -59,38 +59,38 @@ describe('useCourseProductUnion', () => {
|
|
|
59
59
|
fetchMock.restore();
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('should call courses and
|
|
62
|
+
it('should call courses and offer endpoints', async () => {
|
|
63
63
|
const ROUTES = getRoutes();
|
|
64
64
|
const coursesUrl = ROUTES.courses.get.replace(':id/', '');
|
|
65
|
-
const
|
|
65
|
+
const offersUrl = ROUTES.offers.get.replace(':id/', '');
|
|
66
66
|
fetchMock.get(
|
|
67
67
|
`${coursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
|
|
68
68
|
mockPaginatedResponse(courseList.slice(0, PER_PAGE), 0, false),
|
|
69
69
|
);
|
|
70
70
|
fetchMock.get(
|
|
71
|
-
`${
|
|
72
|
-
mockPaginatedResponse(
|
|
71
|
+
`${offersUrl}?page=1&page_size=${PER_PAGE}`,
|
|
72
|
+
mockPaginatedResponse(offerList.slice(0, PER_PAGE), 0, false),
|
|
73
73
|
);
|
|
74
74
|
const { result } = renderUseCourseProductUnion();
|
|
75
75
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
76
76
|
expect(result.current.data.length).toBe(PER_PAGE);
|
|
77
77
|
nbApiCalls += 1; // courses page 1
|
|
78
|
-
nbApiCalls += 1; //
|
|
78
|
+
nbApiCalls += 1; // offers page 1
|
|
79
79
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
80
80
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
81
81
|
expect(calledUrls).toContain(
|
|
82
82
|
`${coursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
|
|
83
83
|
);
|
|
84
|
-
expect(calledUrls).toContain(`${
|
|
84
|
+
expect(calledUrls).toContain(`${offersUrl}?page=1&page_size=${PER_PAGE}`);
|
|
85
85
|
}, 25000);
|
|
86
86
|
|
|
87
|
-
it('should call organization courses and organization
|
|
87
|
+
it('should call organization courses and organization offer endpoints', async () => {
|
|
88
88
|
const organizationId = 'DUMMY_ORGANIZATION_ID';
|
|
89
89
|
const ROUTES = getRoutes();
|
|
90
90
|
const organizationCoursesUrl = ROUTES.organizations.courses.get
|
|
91
91
|
.replace(':organization_id', organizationId)
|
|
92
92
|
.replace(':id/', '');
|
|
93
|
-
const
|
|
93
|
+
const organizationOffersUrl = ROUTES.organizations.offers.get
|
|
94
94
|
.replace(':organization_id', organizationId)
|
|
95
95
|
.replace(':id/', '');
|
|
96
96
|
fetchMock.get(
|
|
@@ -98,21 +98,19 @@ describe('useCourseProductUnion', () => {
|
|
|
98
98
|
mockPaginatedResponse(courseList.slice(0, PER_PAGE), 0, false),
|
|
99
99
|
);
|
|
100
100
|
fetchMock.get(
|
|
101
|
-
`${
|
|
102
|
-
mockPaginatedResponse(
|
|
101
|
+
`${organizationOffersUrl}?page=1&page_size=${PER_PAGE}`,
|
|
102
|
+
mockPaginatedResponse(offerList.slice(0, PER_PAGE), 0, false),
|
|
103
103
|
);
|
|
104
104
|
const { result } = renderUseCourseProductUnion({ organizationId: 'DUMMY_ORGANIZATION_ID' });
|
|
105
105
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
106
106
|
expect(result.current.data.length).toBe(PER_PAGE);
|
|
107
107
|
nbApiCalls += 1; // courses page 1
|
|
108
|
-
nbApiCalls += 1; //
|
|
108
|
+
nbApiCalls += 1; // offers page 1
|
|
109
109
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
110
110
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
111
111
|
expect(calledUrls).toContain(
|
|
112
112
|
`${organizationCoursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
|
|
113
113
|
);
|
|
114
|
-
expect(calledUrls).toContain(
|
|
115
|
-
`${organizationCourseProductRelationsUrl}?page=1&page_size=${PER_PAGE}`,
|
|
116
|
-
);
|
|
114
|
+
expect(calledUrls).toContain(`${organizationOffersUrl}?page=1&page_size=${PER_PAGE}`);
|
|
117
115
|
});
|
|
118
116
|
});
|
|
@@ -3,11 +3,11 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
|
3
3
|
import {
|
|
4
4
|
CourseListItem,
|
|
5
5
|
Product,
|
|
6
|
-
|
|
6
|
+
Offer,
|
|
7
7
|
CourseQueryFilters,
|
|
8
|
-
|
|
8
|
+
OfferQueryFilters,
|
|
9
9
|
ProductType,
|
|
10
|
-
|
|
10
|
+
OfferLight,
|
|
11
11
|
} from 'types/Joanie';
|
|
12
12
|
import useUnionResource, { ResourceUnionPaginationProps } from 'hooks/useUnionResource';
|
|
13
13
|
|
|
@@ -41,9 +41,9 @@ export const useCourseProductUnion = ({
|
|
|
41
41
|
const api = useJoanieApi();
|
|
42
42
|
return useUnionResource<
|
|
43
43
|
CourseListItem,
|
|
44
|
-
|
|
44
|
+
Offer | OfferLight,
|
|
45
45
|
CourseQueryFilters,
|
|
46
|
-
|
|
46
|
+
OfferQueryFilters
|
|
47
47
|
>({
|
|
48
48
|
queryAConfig: {
|
|
49
49
|
queryKey: ['user', 'courses'],
|
|
@@ -51,8 +51,8 @@ export const useCourseProductUnion = ({
|
|
|
51
51
|
filters: { query, organization_id: organizationId, has_listed_course_runs: true },
|
|
52
52
|
},
|
|
53
53
|
queryBConfig: {
|
|
54
|
-
queryKey: ['user', '
|
|
55
|
-
fn: api.
|
|
54
|
+
queryKey: ['user', 'offers'],
|
|
55
|
+
fn: api.offers.get,
|
|
56
56
|
filters: { query, organization_id: organizationId, product_type: productType },
|
|
57
57
|
},
|
|
58
58
|
perPage,
|