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
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
UserFactory,
|
|
12
12
|
} from 'utils/test/factories/richie';
|
|
13
13
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
14
|
-
import {
|
|
14
|
+
import { OfferFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
15
15
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
16
16
|
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
17
17
|
import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
|
|
@@ -46,15 +46,9 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('should render TeacherDashboardTrainingLoader page', async () => {
|
|
49
|
-
const
|
|
50
|
-
fetchMock.get(
|
|
51
|
-
|
|
52
|
-
[],
|
|
53
|
-
);
|
|
54
|
-
fetchMock.get(
|
|
55
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
56
|
-
courseProductRelation,
|
|
57
|
-
);
|
|
49
|
+
const offer = OfferFactory().one();
|
|
50
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/?offer_id=${offer.id}`, []);
|
|
51
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/offers/${offer.id}/`, offer);
|
|
58
52
|
|
|
59
53
|
const user = UserFactory().one();
|
|
60
54
|
render(
|
|
@@ -67,12 +61,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
67
61
|
router={createMemoryRouter(
|
|
68
62
|
[
|
|
69
63
|
{
|
|
70
|
-
path: ':
|
|
64
|
+
path: ':offerId',
|
|
71
65
|
element: <TeacherDashboardTrainingLoader />,
|
|
72
66
|
},
|
|
73
67
|
],
|
|
74
68
|
{
|
|
75
|
-
initialEntries: [`/${
|
|
69
|
+
initialEntries: [`/${offer.id}`],
|
|
76
70
|
},
|
|
77
71
|
)}
|
|
78
72
|
/>
|
|
@@ -88,12 +82,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
88
82
|
await expectNoSpinner('Loading course...');
|
|
89
83
|
|
|
90
84
|
nbApiCalls += 1; // organizations api call
|
|
91
|
-
nbApiCalls += 1; //
|
|
85
|
+
nbApiCalls += 1; // offers api call
|
|
92
86
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
93
87
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
94
|
-
expect(calledUrls).toContain(
|
|
95
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
96
|
-
);
|
|
88
|
+
expect(calledUrls).toContain(`https://joanie.endpoint/api/v1.0/offers/${offer.id}/`);
|
|
97
89
|
|
|
98
90
|
// main titles
|
|
99
91
|
expect(
|
|
@@ -102,28 +94,28 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
102
94
|
}),
|
|
103
95
|
).toBeInTheDocument();
|
|
104
96
|
|
|
105
|
-
expect(
|
|
106
|
-
|
|
107
|
-
)
|
|
97
|
+
expect(screen.getAllByRole('heading', { name: capitalize(offer.product.title) })).toHaveLength(
|
|
98
|
+
2,
|
|
99
|
+
);
|
|
108
100
|
|
|
109
|
-
const nbCourseRun =
|
|
101
|
+
const nbCourseRun = offer.product.target_courses.reduce(
|
|
110
102
|
(acc, course) => acc + course.course_runs.length,
|
|
111
103
|
0,
|
|
112
104
|
);
|
|
113
105
|
expect(screen.getAllByRole('link', { name: 'Go to course area' })).toHaveLength(nbCourseRun);
|
|
114
106
|
});
|
|
115
107
|
|
|
116
|
-
it('should fetch
|
|
108
|
+
it('should fetch offer with organization id if there is one in the path', async () => {
|
|
117
109
|
const organization = OrganizationFactory().one();
|
|
118
|
-
const
|
|
110
|
+
const offer = OfferFactory({
|
|
119
111
|
organizations: [organization],
|
|
120
112
|
}).one();
|
|
121
113
|
fetchMock.get(
|
|
122
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
123
|
-
|
|
114
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offers/${offer.id}/`,
|
|
115
|
+
offer,
|
|
124
116
|
);
|
|
125
117
|
fetchMock.get(
|
|
126
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?
|
|
118
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?offer_id=${offer.id}&signature_state=half_signed&page=1&page_size=25`,
|
|
127
119
|
[],
|
|
128
120
|
);
|
|
129
121
|
|
|
@@ -138,12 +130,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
138
130
|
router={createMemoryRouter(
|
|
139
131
|
[
|
|
140
132
|
{
|
|
141
|
-
path: '/:organizationId/:
|
|
133
|
+
path: '/:organizationId/:offerId',
|
|
142
134
|
element: <TeacherDashboardTrainingLoader />,
|
|
143
135
|
},
|
|
144
136
|
],
|
|
145
137
|
{
|
|
146
|
-
initialEntries: [`/${organization.id}/${
|
|
138
|
+
initialEntries: [`/${organization.id}/${offer.id}`],
|
|
147
139
|
},
|
|
148
140
|
)}
|
|
149
141
|
/>
|
|
@@ -159,11 +151,11 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
159
151
|
await expectNoSpinner('Loading course...');
|
|
160
152
|
|
|
161
153
|
nbApiCalls += 1; // contracts api call
|
|
162
|
-
nbApiCalls += 1; //
|
|
154
|
+
nbApiCalls += 1; // offers api call
|
|
163
155
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
164
156
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
165
157
|
expect(calledUrls).toContain(
|
|
166
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
158
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offers/${offer.id}/`,
|
|
167
159
|
);
|
|
168
160
|
|
|
169
161
|
// main titles
|
|
@@ -173,11 +165,11 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
173
165
|
}),
|
|
174
166
|
).toBeInTheDocument();
|
|
175
167
|
|
|
176
|
-
expect(
|
|
177
|
-
|
|
178
|
-
)
|
|
168
|
+
expect(screen.getAllByRole('heading', { name: capitalize(offer.product.title) })).toHaveLength(
|
|
169
|
+
2,
|
|
170
|
+
);
|
|
179
171
|
|
|
180
|
-
const nbCourseRun =
|
|
172
|
+
const nbCourseRun = offer.product.target_courses.reduce(
|
|
181
173
|
(acc, course) => acc + course.course_runs.length,
|
|
182
174
|
0,
|
|
183
175
|
);
|
|
@@ -6,37 +6,33 @@ import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
|
|
|
6
6
|
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
|
|
7
7
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
8
8
|
import Banner, { BannerType } from 'components/Banner';
|
|
9
|
-
import {
|
|
9
|
+
import { Offer } from 'types/Joanie';
|
|
10
10
|
|
|
11
11
|
const messages = defineMessages({
|
|
12
|
-
|
|
12
|
+
errorNoOffer: {
|
|
13
13
|
defaultMessage: "This product doesn't exist",
|
|
14
|
-
description: 'Message displayed when requested
|
|
15
|
-
id: 'components.TeacherDashboardTraining.
|
|
14
|
+
description: 'Message displayed when requested offer is not found',
|
|
15
|
+
id: 'components.TeacherDashboardTraining.errorNoOffer',
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
interface TeacherDashboardTrainingProps {
|
|
20
|
-
|
|
20
|
+
offer: Offer;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const TeacherDashboardTraining = ({
|
|
24
|
-
courseProductRelation,
|
|
25
|
-
}: TeacherDashboardTrainingProps) => {
|
|
23
|
+
export const TeacherDashboardTraining = ({ offer }: TeacherDashboardTrainingProps) => {
|
|
26
24
|
const intl = useIntl();
|
|
27
|
-
return
|
|
25
|
+
return offer ? (
|
|
28
26
|
<div className="teacher-course-page">
|
|
29
27
|
<DashboardCard
|
|
30
28
|
className="icon-arrow-right-rounded"
|
|
31
29
|
header={
|
|
32
30
|
<div>
|
|
33
31
|
<div className="dashboard__title_container--small">
|
|
34
|
-
<h2 className="dashboard__title--large">
|
|
35
|
-
{capitalize(courseProductRelation.product.title)}
|
|
36
|
-
</h2>
|
|
32
|
+
<h2 className="dashboard__title--large">{capitalize(offer.product.title)}</h2>
|
|
37
33
|
</div>
|
|
38
|
-
{
|
|
39
|
-
<div className="dashboard__quote">{
|
|
34
|
+
{offer.product.description && (
|
|
35
|
+
<div className="dashboard__quote">{offer.product.description}</div>
|
|
40
36
|
)}
|
|
41
37
|
</div>
|
|
42
38
|
}
|
|
@@ -44,7 +40,7 @@ export const TeacherDashboardTraining = ({
|
|
|
44
40
|
fullWidth
|
|
45
41
|
/>
|
|
46
42
|
<DashboardLayout.NestedSection>
|
|
47
|
-
{
|
|
43
|
+
{offer.product.target_courses.map((course) => (
|
|
48
44
|
<DashboardLayout.Section key={`course_target_${course.code}`}>
|
|
49
45
|
<DashboardCard
|
|
50
46
|
className="icon-arrow-right-rounded"
|
|
@@ -66,11 +62,7 @@ export const TeacherDashboardTraining = ({
|
|
|
66
62
|
</DashboardLayout.NestedSection>
|
|
67
63
|
</div>
|
|
68
64
|
) : (
|
|
69
|
-
<Banner
|
|
70
|
-
message={intl.formatMessage(messages.errorNoCourseProductRelation)}
|
|
71
|
-
type={BannerType.ERROR}
|
|
72
|
-
rounded
|
|
73
|
-
/>
|
|
65
|
+
<Banner message={intl.formatMessage(messages.errorNoOffer)} type={BannerType.ERROR} rounded />
|
|
74
66
|
);
|
|
75
67
|
};
|
|
76
68
|
|
package/js/types/Joanie.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Organization {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface OrganizationResourceQuery extends ResourcesQuery {
|
|
41
|
-
|
|
41
|
+
offer_id?: Offer['id'];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface ContractDefinition {
|
|
@@ -174,7 +174,7 @@ export interface DefinitionResourcesProduct {
|
|
|
174
174
|
contract_definition_id: Nullable<ContractDefinition['id']>;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export interface
|
|
177
|
+
export interface OfferLight {
|
|
178
178
|
id: string;
|
|
179
179
|
course: CourseLight;
|
|
180
180
|
organizations: Organization[];
|
|
@@ -182,13 +182,18 @@ export interface CourseProductRelationLight {
|
|
|
182
182
|
created_on: string;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
export interface
|
|
186
|
-
order_groups: OrderGroup[];
|
|
185
|
+
export interface Offer extends OfferLight {
|
|
187
186
|
is_withdrawable: boolean;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
discounted_price: Nullable<number>;
|
|
188
|
+
discount_rate: Nullable<number>;
|
|
189
|
+
discount_amount: Nullable<number>;
|
|
190
|
+
discount_start: Nullable<string>;
|
|
191
|
+
discount_end: Nullable<string>;
|
|
192
|
+
description: Nullable<string>;
|
|
193
|
+
nb_seats_available: Nullable<number>;
|
|
194
|
+
seats: Nullable<number>;
|
|
195
|
+
}
|
|
196
|
+
export function isOffer(entity: CourseListItem | OfferLight | RichieCourse): entity is OfferLight {
|
|
192
197
|
return 'course' in entity && 'product' in entity;
|
|
193
198
|
}
|
|
194
199
|
|
|
@@ -233,7 +238,7 @@ export interface Enrollment {
|
|
|
233
238
|
was_created_by_order: boolean;
|
|
234
239
|
created_on: string;
|
|
235
240
|
orders: OrderEnrollment[];
|
|
236
|
-
|
|
241
|
+
offers: Offer[];
|
|
237
242
|
certificate_id: Nullable<string>;
|
|
238
243
|
}
|
|
239
244
|
export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
|
|
@@ -247,7 +252,7 @@ export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj
|
|
|
247
252
|
'was_created_by_order' in obj &&
|
|
248
253
|
'created_on' in obj &&
|
|
249
254
|
'orders' in obj &&
|
|
250
|
-
'
|
|
255
|
+
'offers' in obj &&
|
|
251
256
|
'certificate_id' in obj
|
|
252
257
|
);
|
|
253
258
|
};
|
|
@@ -325,7 +330,6 @@ export interface Order {
|
|
|
325
330
|
enrollment: Nullable<EnrollmentLight>;
|
|
326
331
|
organization_id: Organization['id'];
|
|
327
332
|
organization: Organization;
|
|
328
|
-
order_group_id?: OrderGroup['id'];
|
|
329
333
|
payment_schedule?: PaymentSchedule;
|
|
330
334
|
credit_card_id?: CreditCard['id'];
|
|
331
335
|
}
|
|
@@ -398,18 +402,11 @@ export interface NestedCourseOrder {
|
|
|
398
402
|
|
|
399
403
|
export interface CourseOrderResourceQuery extends PaginatedResourceQuery {
|
|
400
404
|
course_id?: CourseListItem['id'];
|
|
401
|
-
|
|
405
|
+
offer_id?: Offer['id'];
|
|
402
406
|
organization_id?: Organization['id'];
|
|
403
407
|
product_id?: Product['id'];
|
|
404
408
|
}
|
|
405
409
|
|
|
406
|
-
export interface OrderGroup {
|
|
407
|
-
id: string;
|
|
408
|
-
is_active: boolean;
|
|
409
|
-
nb_seats: number;
|
|
410
|
-
nb_available_seats: number;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
410
|
export enum CreditCardBrand {
|
|
414
411
|
MASTERCARD = 'mastercard',
|
|
415
412
|
MAESTRO = 'maestro',
|
|
@@ -481,7 +478,6 @@ export interface AddressCreationPayload extends Omit<Address, 'id' | 'is_main'>
|
|
|
481
478
|
|
|
482
479
|
interface AbstractOrderProductCreationPayload {
|
|
483
480
|
product_id: Product['id'];
|
|
484
|
-
order_group_id?: OrderGroup['id'];
|
|
485
481
|
billing_address: Omit<Address, 'id' | 'is_main'>;
|
|
486
482
|
has_waived_withdrawal_right: boolean;
|
|
487
483
|
}
|
|
@@ -541,8 +537,8 @@ export interface CourseProductQueryFilters extends ResourcesQuery {
|
|
|
541
537
|
id?: Product['id'];
|
|
542
538
|
course_id?: CourseListItem['id'];
|
|
543
539
|
}
|
|
544
|
-
export interface
|
|
545
|
-
id?:
|
|
540
|
+
export interface OfferQueryFilters extends PaginatedResourceQuery {
|
|
541
|
+
id?: Offer['id'];
|
|
546
542
|
organization_id?: Organization['id'];
|
|
547
543
|
product_type?: ProductType;
|
|
548
544
|
query?: string;
|
|
@@ -555,7 +551,7 @@ export enum ContractState {
|
|
|
555
551
|
}
|
|
556
552
|
export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
557
553
|
organization_id?: Organization['id'];
|
|
558
|
-
|
|
554
|
+
offer_id?: Offer['id'];
|
|
559
555
|
contract_ids?: Contract['id'][];
|
|
560
556
|
signature_state?: ContractState;
|
|
561
557
|
}
|
|
@@ -563,7 +559,7 @@ export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
|
563
559
|
export interface OrganizationContractSignatureLinksFilters {
|
|
564
560
|
contracts_ids?: string[];
|
|
565
561
|
organization_id: Organization['id'];
|
|
566
|
-
|
|
562
|
+
offer_ids?: Offer['id'][];
|
|
567
563
|
}
|
|
568
564
|
|
|
569
565
|
export interface ContractInvitationLinkResponse {
|
|
@@ -657,10 +653,10 @@ interface APIUser {
|
|
|
657
653
|
check: (id: string) => Promise<Response>;
|
|
658
654
|
create: ({
|
|
659
655
|
organization_id,
|
|
660
|
-
|
|
656
|
+
offer_id,
|
|
661
657
|
}: {
|
|
662
658
|
organization_id?: Organization['id'];
|
|
663
|
-
|
|
659
|
+
offer_id?: Offer['id'];
|
|
664
660
|
}) => Promise<{ url: string }>;
|
|
665
661
|
get: (id: string) => Promise<File>;
|
|
666
662
|
};
|
|
@@ -676,7 +672,7 @@ export interface API {
|
|
|
676
672
|
? Promise<Nullable<CourseListItem>>
|
|
677
673
|
: Promise<PaginatedResponse<CourseListItem>>;
|
|
678
674
|
products: {
|
|
679
|
-
get(filters?: CourseProductQueryFilters): Promise<Nullable<
|
|
675
|
+
get(filters?: CourseProductQueryFilters): Promise<Nullable<Offer>>;
|
|
680
676
|
paymentSchedule: {
|
|
681
677
|
get(filters?: CourseProductQueryFilters): Promise<Nullable<PaymentSchedule>>;
|
|
682
678
|
};
|
|
@@ -709,12 +705,12 @@ export interface API {
|
|
|
709
705
|
filters?: CourseRunFilters,
|
|
710
706
|
): CourseRunFilters extends { id: string } ? Promise<Nullable<CourseRun>> : Promise<CourseRun>;
|
|
711
707
|
};
|
|
712
|
-
|
|
708
|
+
offers: {
|
|
713
709
|
get<Filters extends PaginatedResourceQuery = PaginatedResourceQuery>(
|
|
714
710
|
filters?: Filters,
|
|
715
711
|
): Filters extends { id: string }
|
|
716
|
-
? Promise<Nullable<
|
|
717
|
-
: Promise<PaginatedResponse<
|
|
712
|
+
? Promise<Nullable<Offer>>
|
|
713
|
+
: Promise<PaginatedResponse<OfferLight>>;
|
|
718
714
|
};
|
|
719
715
|
contractDefinitions: {
|
|
720
716
|
previewTemplate(id: string): Promise<File>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IntlShape } from 'react-intl';
|
|
2
|
-
import {
|
|
2
|
+
import { Product, TargetCourse } from 'types/Joanie';
|
|
3
3
|
import { Maybe } from 'types/utils';
|
|
4
4
|
import { IntlHelper } from 'utils/IntlHelper';
|
|
5
5
|
import * as Joanie from 'types/Joanie';
|
|
@@ -44,10 +44,6 @@ export class ProductHelper {
|
|
|
44
44
|
return IntlHelper.getLocalizedLanguages(uniqueLanguages, intl);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
static getActiveOrderGroups(courseProductRelation: CourseProductRelation) {
|
|
48
|
-
return courseProductRelation.order_groups?.filter((orderGroup) => orderGroup.is_active);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
static hasRemainingSeats(product: Maybe<Product>) {
|
|
52
48
|
if (!product) return false;
|
|
53
49
|
return typeof product?.remaining_order_count !== 'number' || product.remaining_order_count > 0;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
CourseLight,
|
|
13
13
|
CourseListItem,
|
|
14
14
|
CourseProduct,
|
|
15
|
-
|
|
15
|
+
Offer,
|
|
16
16
|
CourseRun,
|
|
17
17
|
CredentialOrder,
|
|
18
18
|
CredentialProduct,
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
NestedCredentialOrder,
|
|
29
29
|
Order,
|
|
30
30
|
OrderEnrollment,
|
|
31
|
-
OrderGroup,
|
|
32
31
|
PaymentInstallment,
|
|
33
32
|
OrderLite,
|
|
34
33
|
OrderState,
|
|
@@ -90,7 +89,7 @@ export const EnrollmentFactory = factory((): Enrollment => {
|
|
|
90
89
|
id: faker.string.uuid(),
|
|
91
90
|
course_run: CourseRunWithCourseFactory().one(),
|
|
92
91
|
is_active: true,
|
|
93
|
-
|
|
92
|
+
offers: OfferFactory().many(1),
|
|
94
93
|
state: EnrollmentState.SET,
|
|
95
94
|
was_created_by_order: false,
|
|
96
95
|
created_on: faker.date.past({ years: 1 }).toISOString(),
|
|
@@ -197,7 +196,7 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
|
|
|
197
196
|
created_on: faker.date.past().toISOString(),
|
|
198
197
|
title: FactoryHelper.sequence((counter) => `Certificate Product ${counter}`),
|
|
199
198
|
type: ProductType.CREDENTIAL,
|
|
200
|
-
price: faker.number.int(),
|
|
199
|
+
price: faker.number.int({ min: 1, max: 1000, multipleOf: 10 }),
|
|
201
200
|
price_currency: faker.finance.currencyCode(),
|
|
202
201
|
call_to_action: faker.lorem.words(3),
|
|
203
202
|
certificate_definition: CertificationDefinitionFactory().one(),
|
|
@@ -294,25 +293,6 @@ export const CourseLightFactory = factory((): CourseLight => {
|
|
|
294
293
|
};
|
|
295
294
|
});
|
|
296
295
|
|
|
297
|
-
export const OrderGroupFactory = factory((): OrderGroup => {
|
|
298
|
-
const seats = faker.number.int({ min: 5, max: 100 });
|
|
299
|
-
return {
|
|
300
|
-
id: faker.string.uuid(),
|
|
301
|
-
is_active: true,
|
|
302
|
-
nb_seats: seats,
|
|
303
|
-
nb_available_seats: faker.number.int({ min: 2, max: seats }),
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
export const OrderGroupFullFactory = factory((): OrderGroup => {
|
|
308
|
-
return {
|
|
309
|
-
id: faker.string.uuid(),
|
|
310
|
-
is_active: true,
|
|
311
|
-
nb_seats: faker.number.int({ min: 5, max: 100 }),
|
|
312
|
-
nb_available_seats: 0,
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
|
|
316
296
|
export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
317
297
|
return {
|
|
318
298
|
id: faker.string.uuid(),
|
|
@@ -329,15 +309,22 @@ export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
|
329
309
|
};
|
|
330
310
|
});
|
|
331
311
|
|
|
332
|
-
export const
|
|
312
|
+
export const OfferFactory = factory((): Offer => {
|
|
333
313
|
return {
|
|
334
314
|
id: faker.string.uuid(),
|
|
335
315
|
created_on: faker.date.past().toISOString(),
|
|
336
316
|
course: CourseFactory().one(),
|
|
337
317
|
product: ProductFactory().one(),
|
|
338
318
|
organizations: OrganizationFactory().many(1),
|
|
339
|
-
order_groups: [],
|
|
340
319
|
is_withdrawable: true,
|
|
320
|
+
discounted_price: null,
|
|
321
|
+
discount_rate: null,
|
|
322
|
+
discount_amount: null,
|
|
323
|
+
discount_start: null,
|
|
324
|
+
discount_end: null,
|
|
325
|
+
description: null,
|
|
326
|
+
seats: null,
|
|
327
|
+
nb_seats_available: null,
|
|
341
328
|
};
|
|
342
329
|
});
|
|
343
330
|
|
|
@@ -3,14 +3,14 @@ import { CredentialOrder } from 'types/Joanie';
|
|
|
3
3
|
import {
|
|
4
4
|
ContractDefinitionFactory,
|
|
5
5
|
CourseFactory,
|
|
6
|
-
|
|
6
|
+
OfferFactory,
|
|
7
7
|
ProductFactory,
|
|
8
8
|
} from 'utils/test/factories/joanie';
|
|
9
9
|
|
|
10
10
|
export const mockCourseProductWithOrder = (order: CredentialOrder) => {
|
|
11
11
|
const courseCode = order.course.code;
|
|
12
12
|
const productId = order.product_id;
|
|
13
|
-
const
|
|
13
|
+
const offer = OfferFactory({
|
|
14
14
|
product: ProductFactory({
|
|
15
15
|
id: order.product_id,
|
|
16
16
|
contract_definition: order.contract ? ContractDefinitionFactory().one() : undefined,
|
|
@@ -22,7 +22,7 @@ export const mockCourseProductWithOrder = (order: CredentialOrder) => {
|
|
|
22
22
|
|
|
23
23
|
fetchMock.get(
|
|
24
24
|
`https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${productId}/`,
|
|
25
|
-
|
|
25
|
+
offer,
|
|
26
26
|
);
|
|
27
|
-
return
|
|
27
|
+
return offer;
|
|
28
28
|
};
|
package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx
CHANGED
|
@@ -33,7 +33,7 @@ export const DashboardItemEnrollment = ({ enrollment }: DashboardItemCourseRunPr
|
|
|
33
33
|
</div>
|
|
34
34
|
</div>,
|
|
35
35
|
];
|
|
36
|
-
enrollment.
|
|
36
|
+
enrollment.offers.forEach(({ product, is_withdrawable }) => {
|
|
37
37
|
if (isCertificateProduct(product)) {
|
|
38
38
|
partialFooterList.push(
|
|
39
39
|
<ProductCertificateFooter
|
|
@@ -249,7 +249,7 @@ describe('<ProductCertificateFooter/>', () => {
|
|
|
249
249
|
course,
|
|
250
250
|
}).one(),
|
|
251
251
|
}).one();
|
|
252
|
-
enrollment.
|
|
252
|
+
enrollment.offers[0].product = CertificateProductFactory().one();
|
|
253
253
|
|
|
254
254
|
fetchMock.get(
|
|
255
255
|
`https://joanie.endpoint/api/v1.0/enrollments/?was_created_by_order=false&is_active=true&page=1&page_size=${PER_PAGE.useOrdersEnrollments}`,
|
|
@@ -67,15 +67,15 @@ export const DashboardItemOrder = ({
|
|
|
67
67
|
}: DashboardItemOrderProps) => {
|
|
68
68
|
const { course } = order;
|
|
69
69
|
const intl = useIntl();
|
|
70
|
-
const { item:
|
|
70
|
+
const { item: offer } = useCourseProduct({
|
|
71
71
|
product_id: order.product_id,
|
|
72
72
|
course_id: course.code,
|
|
73
73
|
});
|
|
74
|
-
const { product } =
|
|
74
|
+
const { product } = offer || {};
|
|
75
75
|
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
76
76
|
const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
|
|
77
77
|
const isActive = OrderHelper.isActive(order);
|
|
78
|
-
const isProductPurchasable = ProductHelper.isPurchasable(
|
|
78
|
+
const isProductPurchasable = ProductHelper.isPurchasable(offer?.product);
|
|
79
79
|
const isNotResumable = !isActive && !isProductPurchasable;
|
|
80
80
|
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
81
81
|
|
|
@@ -57,7 +57,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
57
57
|
contract: ContractFactory({ student_signed_on: null }).one(),
|
|
58
58
|
}).one();
|
|
59
59
|
|
|
60
|
-
// learner dashboard course page do one call to
|
|
60
|
+
// learner dashboard course page do one call to offer per order
|
|
61
61
|
const { product } = mockCourseProductWithOrder(order);
|
|
62
62
|
|
|
63
63
|
// overwrite useOmniscientOrders call
|
|
@@ -94,7 +94,7 @@ const Installment = ({ order }: Props) => {
|
|
|
94
94
|
|
|
95
95
|
const PaymentMethodManager = ({ order }: Props) => {
|
|
96
96
|
const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
|
|
97
|
-
const { item:
|
|
97
|
+
const { item: offer, states } = useCourseProduct({
|
|
98
98
|
course_id: order.course.code,
|
|
99
99
|
product_id: order.product_id,
|
|
100
100
|
});
|
|
@@ -118,9 +118,9 @@ const PaymentMethodManager = ({ order }: Props) => {
|
|
|
118
118
|
)}
|
|
119
119
|
<SaleTunnel
|
|
120
120
|
{...modal}
|
|
121
|
-
product={
|
|
122
|
-
course={
|
|
123
|
-
isWithdrawable={
|
|
121
|
+
product={offer.product as CredentialProduct}
|
|
122
|
+
course={offer.course}
|
|
123
|
+
isWithdrawable={offer.is_withdrawable}
|
|
124
124
|
/>
|
|
125
125
|
</>
|
|
126
126
|
);
|