richie-education 3.1.3-dev15 → 3.1.3-dev17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +12 -11
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +28 -22
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -3
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +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 -16
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -4
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -4
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +17 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -8
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +6 -3
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -10
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -8
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -21
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +19 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +6 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +7 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -5
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +55 -55
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -25
- package/js/pages/TeacherDashboardTraining/index.tsx +16 -12
- package/js/types/Joanie.ts +21 -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 -23
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +20 -17
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +22 -16
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -3
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +14 -10
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +87 -63
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +24 -20
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useOffer/index.ts +0 -32
package/js/api/joanie.ts
CHANGED
|
@@ -127,8 +127,8 @@ export const getRoutes = () => {
|
|
|
127
127
|
},
|
|
128
128
|
organizations: {
|
|
129
129
|
get: `${baseUrl}/organizations/:id/`,
|
|
130
|
-
|
|
131
|
-
get: `${baseUrl}/organizations/:organization_id/
|
|
130
|
+
offerings: {
|
|
131
|
+
get: `${baseUrl}/organizations/:organization_id/offerings/:id/`,
|
|
132
132
|
},
|
|
133
133
|
courses: {
|
|
134
134
|
get: `${baseUrl}/organizations/:organization_id/courses/:id/`,
|
|
@@ -156,8 +156,8 @@ export const getRoutes = () => {
|
|
|
156
156
|
courseRuns: {
|
|
157
157
|
get: `${baseUrl}/course-runs/:id/`,
|
|
158
158
|
},
|
|
159
|
-
|
|
160
|
-
get: `${baseUrl}/
|
|
159
|
+
offerings: {
|
|
160
|
+
get: `${baseUrl}/offerings/:id/`,
|
|
161
161
|
},
|
|
162
162
|
contractDefinitions: {
|
|
163
163
|
previewTemplate: `${baseUrl}/contract_definitions/:id/preview_template/`,
|
|
@@ -470,12 +470,12 @@ const API = (): Joanie.API => {
|
|
|
470
470
|
).then(checkStatus);
|
|
471
471
|
},
|
|
472
472
|
},
|
|
473
|
-
|
|
474
|
-
get: (filters?: Joanie.
|
|
473
|
+
offerings: {
|
|
474
|
+
get: (filters?: Joanie.OfferingQueryFilters) => {
|
|
475
475
|
return fetchWithJWT(
|
|
476
476
|
filters?.organization_id
|
|
477
|
-
? buildApiUrl(ROUTES.organizations.
|
|
478
|
-
: buildApiUrl(ROUTES.
|
|
477
|
+
? buildApiUrl(ROUTES.organizations.offerings.get, filters)
|
|
478
|
+
: buildApiUrl(ROUTES.offerings.get, filters),
|
|
479
479
|
).then(checkStatus);
|
|
480
480
|
},
|
|
481
481
|
},
|
|
@@ -6,10 +6,10 @@ import { IntlProvider } from 'react-intl';
|
|
|
6
6
|
import fetchMock from 'fetch-mock';
|
|
7
7
|
import { QueryStateFactory } from 'utils/test/factories/reactQuery';
|
|
8
8
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
-
import { ContractFactory,
|
|
9
|
+
import { ContractFactory, OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
10
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
11
11
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
12
|
-
import {
|
|
12
|
+
import { isOffering } from 'types/Joanie';
|
|
13
13
|
import { Props } from './AbstractContractFrame';
|
|
14
14
|
import { OrganizationContractFrame } from '.';
|
|
15
15
|
|
|
@@ -73,28 +73,29 @@ describe('OrganizationContractFrame', () => {
|
|
|
73
73
|
|
|
74
74
|
it.each([
|
|
75
75
|
{
|
|
76
|
-
label: 'contractList: undefined,
|
|
76
|
+
label: 'contractList: undefined, offering: undefined',
|
|
77
77
|
contractList: undefined,
|
|
78
|
-
|
|
78
|
+
offering: undefined,
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
|
-
label: 'contractList: 2 Contract,
|
|
81
|
+
label: 'contractList: 2 Contract, offering: undefined',
|
|
82
82
|
contractList: ContractFactory().many(2),
|
|
83
|
-
|
|
83
|
+
offering: undefined,
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
|
-
label: 'contractList: undefined,
|
|
86
|
+
label: 'contractList: undefined, offering: one Offering',
|
|
87
87
|
contractList: undefined,
|
|
88
|
-
|
|
88
|
+
offering: OfferingFactory().one(),
|
|
89
89
|
},
|
|
90
90
|
])(
|
|
91
91
|
'should implement AbstractContractFrame for organization and $label',
|
|
92
|
-
async ({ contractList,
|
|
92
|
+
async ({ contractList, offering }) => {
|
|
93
93
|
const organization = OrganizationFactory().one();
|
|
94
94
|
const contracts = contractList || ContractFactory().many(2);
|
|
95
95
|
const isOpen = faker.datatype.boolean();
|
|
96
96
|
|
|
97
|
-
const invitationLinkQueryString =
|
|
97
|
+
const invitationLinkQueryString =
|
|
98
|
+
offering && isOffering(offering) ? `?offering_ids=${offering.id}` : '';
|
|
98
99
|
const expectedUrls = {
|
|
99
100
|
getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
|
|
100
101
|
checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
|
|
@@ -126,7 +127,7 @@ describe('OrganizationContractFrame', () => {
|
|
|
126
127
|
<Wrapper client={client}>
|
|
127
128
|
<OrganizationContractFrame
|
|
128
129
|
organizationId={organization.id}
|
|
129
|
-
|
|
130
|
+
offeringIds={offering ? [offering.id] : undefined}
|
|
130
131
|
isOpen={isOpen}
|
|
131
132
|
onDone={handleDone}
|
|
132
133
|
onClose={handleClose}
|
|
@@ -4,17 +4,17 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
|
4
4
|
import AbstractContractFrame, {
|
|
5
5
|
AbstractProps,
|
|
6
6
|
} from 'components/ContractFrame/AbstractContractFrame';
|
|
7
|
-
import { Contract,
|
|
7
|
+
import { Contract, Offering } from 'types/Joanie';
|
|
8
8
|
|
|
9
9
|
interface Props extends AbstractProps {
|
|
10
10
|
contractIds?: Contract['id'][];
|
|
11
11
|
organizationId: string;
|
|
12
|
-
|
|
12
|
+
offeringIds?: Offering['id'][];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const OrganizationContractFrame = ({
|
|
16
16
|
organizationId,
|
|
17
|
-
|
|
17
|
+
offeringIds = [],
|
|
18
18
|
contractIds,
|
|
19
19
|
onDone,
|
|
20
20
|
...props
|
|
@@ -29,7 +29,7 @@ const OrganizationContractFrame = ({
|
|
|
29
29
|
be signed. We need to keep track of these ids to check if all contracts have been signed.
|
|
30
30
|
*/
|
|
31
31
|
const response = await api.organizations.contracts.getSignatureLinks({
|
|
32
|
-
|
|
32
|
+
offering_ids: offeringIds,
|
|
33
33
|
organization_id: organizationId,
|
|
34
34
|
contracts_ids: contractIds,
|
|
35
35
|
});
|
|
@@ -6,18 +6,23 @@ import {
|
|
|
6
6
|
Course as RichieCourse,
|
|
7
7
|
isRichieCourse,
|
|
8
8
|
} from 'types/Course';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CourseListItem as JoanieCourse,
|
|
11
|
+
OfferingLight,
|
|
12
|
+
isOffering,
|
|
13
|
+
ProductType,
|
|
14
|
+
} from 'types/Joanie';
|
|
10
15
|
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
|
|
11
16
|
import { CourseGlimpseCourse } from '.';
|
|
12
17
|
|
|
13
|
-
const
|
|
14
|
-
|
|
18
|
+
const getCourseGlimpsePropsFromOffering = (
|
|
19
|
+
offering: OfferingLight,
|
|
15
20
|
intl: IntlShape,
|
|
16
21
|
organizationId?: string,
|
|
17
22
|
): CourseGlimpseCourse => {
|
|
18
23
|
const courseRouteParams = {
|
|
19
|
-
courseId:
|
|
20
|
-
|
|
24
|
+
courseId: offering.course.id,
|
|
25
|
+
offeringId: offering.id,
|
|
21
26
|
};
|
|
22
27
|
const courseRoute = organizationId
|
|
23
28
|
? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
|
|
@@ -26,27 +31,28 @@ const getCourseGlimpsePropsFromOffer = (
|
|
|
26
31
|
})
|
|
27
32
|
: generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
|
|
28
33
|
return {
|
|
29
|
-
id:
|
|
30
|
-
code:
|
|
31
|
-
title:
|
|
32
|
-
cover_image:
|
|
34
|
+
id: offering.id,
|
|
35
|
+
code: offering.course.code,
|
|
36
|
+
title: offering.product.title,
|
|
37
|
+
cover_image: offering.course.cover
|
|
33
38
|
? {
|
|
34
|
-
src:
|
|
39
|
+
src: offering.course.cover.src,
|
|
35
40
|
}
|
|
36
41
|
: null,
|
|
37
42
|
organization: {
|
|
38
|
-
title:
|
|
39
|
-
image:
|
|
43
|
+
title: offering.organizations[0].title,
|
|
44
|
+
image: offering.organizations[0].logo || null,
|
|
40
45
|
},
|
|
41
|
-
product_id:
|
|
46
|
+
product_id: offering.product.id,
|
|
42
47
|
course_route: courseRoute,
|
|
43
|
-
state:
|
|
48
|
+
state: offering.product.state,
|
|
44
49
|
certificate_offer:
|
|
45
|
-
|
|
46
|
-
offer:
|
|
47
|
-
certificate_price:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
offering.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
|
|
51
|
+
offer: offering.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
|
|
52
|
+
certificate_price:
|
|
53
|
+
offering.product.type === ProductType.CERTIFICATE ? offering.product.price : null,
|
|
54
|
+
price: offering.product.type === ProductType.CREDENTIAL ? offering.product.price : null,
|
|
55
|
+
price_currency: offering.product.price_currency,
|
|
50
56
|
};
|
|
51
57
|
};
|
|
52
58
|
|
|
@@ -112,12 +118,12 @@ const getCourseGlimpsePropsFromJoanieCourse = (
|
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
export const getCourseGlimpseProps = (
|
|
115
|
-
course: RichieCourse | (JoanieCourse |
|
|
121
|
+
course: RichieCourse | (JoanieCourse | OfferingLight),
|
|
116
122
|
intl?: IntlShape,
|
|
117
123
|
organizationId?: string,
|
|
118
124
|
): CourseGlimpseCourse => {
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
125
|
+
if (isOffering(course)) {
|
|
126
|
+
return getCourseGlimpsePropsFromOffering(course, intl!, organizationId);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
if (isRichieCourse(course)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IntlShape } from 'react-intl';
|
|
2
|
-
import {
|
|
2
|
+
import { OfferingLight, CourseListItem as JoanieCourse } from 'types/Joanie';
|
|
3
3
|
import { Course as RichieCourse } from 'types/Course';
|
|
4
4
|
import { CourseGlimpseCourse, getCourseGlimpseProps } from 'components/CourseGlimpse';
|
|
5
5
|
|
|
6
6
|
export const getCourseGlimpseListProps = (
|
|
7
|
-
courses: RichieCourse[] | (JoanieCourse |
|
|
7
|
+
courses: RichieCourse[] | (JoanieCourse | OfferingLight)[],
|
|
8
8
|
intl?: IntlShape,
|
|
9
9
|
organizationId?: string,
|
|
10
10
|
): CourseGlimpseCourse[] => {
|
|
@@ -42,7 +42,7 @@ const messages = defineMessages({
|
|
|
42
42
|
|
|
43
43
|
interface PurchaseButtonPropsBase {
|
|
44
44
|
product: Joanie.CredentialProduct | Joanie.CertificateProduct;
|
|
45
|
-
|
|
45
|
+
offering?: Joanie.Offering;
|
|
46
46
|
isWithdrawable: boolean;
|
|
47
47
|
disabled?: boolean;
|
|
48
48
|
className?: string;
|
|
@@ -66,7 +66,7 @@ interface CertificatePurchaseButtonProps extends PurchaseButtonPropsBase {
|
|
|
66
66
|
const PurchaseButton = ({
|
|
67
67
|
product,
|
|
68
68
|
course,
|
|
69
|
-
|
|
69
|
+
offering,
|
|
70
70
|
enrollment,
|
|
71
71
|
isWithdrawable,
|
|
72
72
|
organizations,
|
|
@@ -140,7 +140,7 @@ const PurchaseButton = ({
|
|
|
140
140
|
{...saleTunnelModal}
|
|
141
141
|
product={product}
|
|
142
142
|
organizations={organizations}
|
|
143
|
-
|
|
143
|
+
offering={offering}
|
|
144
144
|
enrollment={enrollment}
|
|
145
145
|
course={course}
|
|
146
146
|
isWithdrawable={isWithdrawable}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
|
|
12
12
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
13
|
-
import { Address,
|
|
13
|
+
import { Address, Offering, CreditCard, Order, OrderState, Product } from 'types/Joanie';
|
|
14
14
|
import useProductOrder from 'hooks/useProductOrder';
|
|
15
15
|
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
|
|
16
16
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
@@ -26,7 +26,7 @@ export interface SaleTunnelContextType {
|
|
|
26
26
|
order?: Order;
|
|
27
27
|
product: Product;
|
|
28
28
|
webAnalyticsEventKey: string;
|
|
29
|
-
|
|
29
|
+
offering?: Offering;
|
|
30
30
|
|
|
31
31
|
// internal
|
|
32
32
|
step: SaleTunnelStep;
|
|
@@ -114,7 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
114
114
|
webAnalyticsEventKey: props.eventKey,
|
|
115
115
|
order,
|
|
116
116
|
product: props.product,
|
|
117
|
-
|
|
117
|
+
offering: props.offering,
|
|
118
118
|
props,
|
|
119
119
|
billingAddress,
|
|
120
120
|
setBillingAddress,
|
|
@@ -101,7 +101,7 @@ const Email = () => {
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const Total = () => {
|
|
104
|
-
const { product,
|
|
104
|
+
const { product, offering } = useSaleTunnelContext();
|
|
105
105
|
return (
|
|
106
106
|
<div className="sale-tunnel__total">
|
|
107
107
|
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
|
|
@@ -110,7 +110,7 @@ const Total = () => {
|
|
|
110
110
|
</div>
|
|
111
111
|
<div className="block-title">
|
|
112
112
|
<FormattedNumber
|
|
113
|
-
value={
|
|
113
|
+
value={offering?.rules.discounted_price || product.price}
|
|
114
114
|
style="currency"
|
|
115
115
|
currency={product.price_currency}
|
|
116
116
|
/>
|
|
@@ -15,7 +15,7 @@ import CourseProductItem from 'widgets/SyllabusCourseRunsList/components/CourseP
|
|
|
15
15
|
import {
|
|
16
16
|
AddressFactory,
|
|
17
17
|
ContractFactory,
|
|
18
|
-
|
|
18
|
+
OfferingFactory,
|
|
19
19
|
CredentialOrderFactory,
|
|
20
20
|
CreditCardFactory,
|
|
21
21
|
PaymentFactory,
|
|
@@ -99,7 +99,7 @@ describe('SaleTunnel', () => {
|
|
|
99
99
|
*/
|
|
100
100
|
const course = PacedCourseFactory().one();
|
|
101
101
|
const product = ProductFactory().one();
|
|
102
|
-
const
|
|
102
|
+
const offering = OfferingFactory({
|
|
103
103
|
course,
|
|
104
104
|
product,
|
|
105
105
|
is_withdrawable: false,
|
|
@@ -108,7 +108,7 @@ describe('SaleTunnel', () => {
|
|
|
108
108
|
|
|
109
109
|
fetchMock.get(
|
|
110
110
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
|
|
111
|
-
|
|
111
|
+
offering,
|
|
112
112
|
);
|
|
113
113
|
fetchMock.get(
|
|
114
114
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
AddressFactory,
|
|
16
16
|
CertificateOrderFactory,
|
|
17
17
|
CertificateProductFactory,
|
|
18
|
-
|
|
18
|
+
OfferingFactory,
|
|
19
19
|
CredentialOrderFactory,
|
|
20
20
|
CredentialProductFactory,
|
|
21
21
|
CreditCardFactory,
|
|
@@ -453,7 +453,7 @@ describe.each([
|
|
|
453
453
|
const intl = createIntl({ locale: 'en' });
|
|
454
454
|
const schedule = PaymentInstallmentFactory().many(2);
|
|
455
455
|
|
|
456
|
-
const
|
|
456
|
+
const offering = OfferingFactory({
|
|
457
457
|
product: ProductFactory({
|
|
458
458
|
price: 840,
|
|
459
459
|
price_currency: 'EUR',
|
|
@@ -463,7 +463,7 @@ describe.each([
|
|
|
463
463
|
discount_rate: 0.3,
|
|
464
464
|
},
|
|
465
465
|
}).one();
|
|
466
|
-
const { product } =
|
|
466
|
+
const { product } = offering;
|
|
467
467
|
|
|
468
468
|
fetchMock
|
|
469
469
|
.get(
|
|
@@ -475,7 +475,7 @@ describe.each([
|
|
|
475
475
|
schedule,
|
|
476
476
|
);
|
|
477
477
|
|
|
478
|
-
render(<Wrapper product={product}
|
|
478
|
+
render(<Wrapper product={product} offering={offering} isWithdrawable={true} />, {
|
|
479
479
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
480
480
|
});
|
|
481
481
|
|
|
@@ -515,7 +515,7 @@ describe.each([
|
|
|
515
515
|
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
516
516
|
expect($totalAmount).toHaveTextContent(
|
|
517
517
|
'Total' +
|
|
518
|
-
formatPrice(
|
|
518
|
+
formatPrice(offering!.rules.discounted_price!, product.price_currency).replace(
|
|
519
519
|
/(\u202F|\u00a0)/g,
|
|
520
520
|
' ',
|
|
521
521
|
),
|
|
@@ -2,7 +2,7 @@ import { ModalProps } from '@openfun/cunningham-react';
|
|
|
2
2
|
import {
|
|
3
3
|
CertificateProduct,
|
|
4
4
|
CourseLight,
|
|
5
|
-
|
|
5
|
+
Offering,
|
|
6
6
|
CredentialProduct,
|
|
7
7
|
Enrollment,
|
|
8
8
|
Order,
|
|
@@ -16,7 +16,7 @@ import { PacedCourse } from 'types';
|
|
|
16
16
|
|
|
17
17
|
export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
|
|
18
18
|
product: Product;
|
|
19
|
-
|
|
19
|
+
offering?: Offering;
|
|
20
20
|
organizations?: Organization[];
|
|
21
21
|
isWithdrawable: boolean;
|
|
22
22
|
course?: PacedCourse | CourseLight;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { screen } from '@testing-library/react';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
3
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
4
|
-
import { CourseListItemFactory,
|
|
4
|
+
import { CourseListItemFactory, OfferingFactory } from 'utils/test/factories/joanie';
|
|
5
5
|
import { render } from 'utils/test/render';
|
|
6
6
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
7
7
|
import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
|
|
@@ -35,7 +35,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('should render loading more state', async () => {
|
|
38
|
-
const trainings =
|
|
38
|
+
const trainings = OfferingFactory().many(2);
|
|
39
39
|
const courses = CourseListItemFactory().many(2);
|
|
40
40
|
const courseAndProductList = [...courses, ...trainings];
|
|
41
41
|
|
|
@@ -60,7 +60,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('should render courses and products list', async () => {
|
|
63
|
-
const trainings =
|
|
63
|
+
const trainings = OfferingFactory().many(2);
|
|
64
64
|
const courses = CourseListItemFactory().many(2);
|
|
65
65
|
const courseAndProductList = [...courses, ...trainings];
|
|
66
66
|
|
|
@@ -6,7 +6,7 @@ import { CourseGlimpseList, getCourseGlimpseListProps } from 'components/CourseG
|
|
|
6
6
|
import { Spinner } from 'components/Spinner';
|
|
7
7
|
import context from 'utils/context';
|
|
8
8
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
|
9
|
-
import { CourseListItem,
|
|
9
|
+
import { CourseListItem, OfferingLight } from 'types/Joanie';
|
|
10
10
|
import Banner from 'components/Banner';
|
|
11
11
|
|
|
12
12
|
const messages = defineMessages({
|
|
@@ -31,7 +31,7 @@ interface TeacherDashboardCourseListProps {
|
|
|
31
31
|
titleTranslated?: string;
|
|
32
32
|
organizationId?: string;
|
|
33
33
|
loadMore: () => void;
|
|
34
|
-
courseAndProductList?: (CourseListItem |
|
|
34
|
+
courseAndProductList?: (CourseListItem | OfferingLight)[];
|
|
35
35
|
isLoadingMore?: boolean;
|
|
36
36
|
hasMore?: boolean;
|
|
37
37
|
isNewSearchLoading?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
2
|
-
import {
|
|
2
|
+
import { Offering, Organization } from 'types/Joanie';
|
|
3
3
|
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
4
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
5
5
|
import { handle } from 'utils/errors/handle';
|
|
@@ -53,11 +53,11 @@ const useContractArchive = () => {
|
|
|
53
53
|
},
|
|
54
54
|
create: async (
|
|
55
55
|
organizationId?: Organization['id'],
|
|
56
|
-
|
|
56
|
+
offeringId?: Offering['id'],
|
|
57
57
|
): Promise<string> => {
|
|
58
58
|
const response = await api.user.contracts.zip_archive.create({
|
|
59
59
|
organization_id: organizationId,
|
|
60
|
-
|
|
60
|
+
offering_id: offeringId,
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
return extractArchiveId(response.url);
|
|
@@ -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, Offering } 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, OfferingFactory } 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 offeringList: Offering[];
|
|
45
45
|
let nbApiCalls: number;
|
|
46
46
|
|
|
47
47
|
beforeEach(() => {
|
|
48
48
|
courseList = CourseListItemFactory().many(6);
|
|
49
|
-
|
|
49
|
+
offeringList = OfferingFactory().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 offering endpoints', async () => {
|
|
63
63
|
const ROUTES = getRoutes();
|
|
64
64
|
const coursesUrl = ROUTES.courses.get.replace(':id/', '');
|
|
65
|
-
const
|
|
65
|
+
const offeringsUrl = ROUTES.offerings.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
|
+
`${offeringsUrl}?page=1&page_size=${PER_PAGE}`,
|
|
72
|
+
mockPaginatedResponse(offeringList.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; // offerings 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(`${offeringsUrl}?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 offering 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 organizationOfferingsUrl = ROUTES.organizations.offerings.get
|
|
94
94
|
.replace(':organization_id', organizationId)
|
|
95
95
|
.replace(':id/', '');
|
|
96
96
|
fetchMock.get(
|
|
@@ -98,19 +98,19 @@ describe('useCourseProductUnion', () => {
|
|
|
98
98
|
mockPaginatedResponse(courseList.slice(0, PER_PAGE), 0, false),
|
|
99
99
|
);
|
|
100
100
|
fetchMock.get(
|
|
101
|
-
`${
|
|
102
|
-
mockPaginatedResponse(
|
|
101
|
+
`${organizationOfferingsUrl}?page=1&page_size=${PER_PAGE}`,
|
|
102
|
+
mockPaginatedResponse(offeringList.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; // offerings 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(`${
|
|
114
|
+
expect(calledUrls).toContain(`${organizationOfferingsUrl}?page=1&page_size=${PER_PAGE}`);
|
|
115
115
|
});
|
|
116
116
|
});
|
|
@@ -3,11 +3,11 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
|
3
3
|
import {
|
|
4
4
|
CourseListItem,
|
|
5
5
|
Product,
|
|
6
|
-
|
|
6
|
+
Offering,
|
|
7
7
|
CourseQueryFilters,
|
|
8
|
-
|
|
8
|
+
OfferingQueryFilters,
|
|
9
9
|
ProductType,
|
|
10
|
-
|
|
10
|
+
OfferingLight,
|
|
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
|
+
Offering | OfferingLight,
|
|
45
45
|
CourseQueryFilters,
|
|
46
|
-
|
|
46
|
+
OfferingQueryFilters
|
|
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', 'offerings'],
|
|
55
|
+
fn: api.offerings.get,
|
|
56
56
|
filters: { query, organization_id: organizationId, product_type: productType },
|
|
57
57
|
},
|
|
58
58
|
perPage,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineMessages } from 'react-intl';
|
|
2
|
-
import { API, CourseProductQueryFilters,
|
|
2
|
+
import { API, CourseProductQueryFilters, Offering, Product } from 'types/Joanie';
|
|
3
3
|
import { QueryOptions, useResourcesCustom, UseResourcesProps } from 'hooks/useResources';
|
|
4
4
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ export const messages = defineMessages({
|
|
|
19
19
|
/**
|
|
20
20
|
* Joanie Api hook to retrieve a product through its id and a course code.
|
|
21
21
|
*/
|
|
22
|
-
const props: UseResourcesProps<
|
|
22
|
+
const props: UseResourcesProps<Offering, CourseProductQueryFilters, API['courses']['products']> = {
|
|
23
23
|
queryKey: ['courses-products'],
|
|
24
24
|
apiInterface: () => useJoanieApi().courses.products,
|
|
25
25
|
messages,
|
|
@@ -27,11 +27,11 @@ const props: UseResourcesProps<Offer, CourseProductQueryFilters, API['courses'][
|
|
|
27
27
|
|
|
28
28
|
export const useCourseProduct = (
|
|
29
29
|
filters: Omit<CourseProductQueryFilters, 'id'> & { product_id: Product['id'] },
|
|
30
|
-
queryOptions?: QueryOptions<
|
|
30
|
+
queryOptions?: QueryOptions<Offering>,
|
|
31
31
|
) => {
|
|
32
32
|
const { product_id: productId, ...queryfilters } = filters;
|
|
33
33
|
const enabled = !!productId && !!queryfilters.course_id;
|
|
34
|
-
const resources = useResourcesCustom<
|
|
34
|
+
const resources = useResourcesCustom<Offering, CourseProductQueryFilters>({
|
|
35
35
|
...props,
|
|
36
36
|
filters: { id: productId, ...queryfilters },
|
|
37
37
|
queryOptions: { ...queryOptions, enabled },
|