richie-education 3.1.3-dev2 → 3.1.3-dev23
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 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/index.spec.tsx +2 -0
- package/js/components/CourseGlimpse/index.tsx +2 -0
- package/js/components/CourseGlimpse/utils.ts +29 -30
- 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 +5 -3
- 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 +116 -28
- 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/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- 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 -17
- 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 -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 +4 -4
- 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 +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/types/Course.ts +2 -0
- package/js/types/Joanie.ts +34 -29
- package/js/types/index.ts +4 -2
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +17 -25
- package/js/utils/test/factories/richie.ts +6 -2
- 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 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- 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 +19 -34
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +34 -8
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
- 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 { OfferingFactory, 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 offering = OfferingFactory().one();
|
|
50
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/?offering_id=${offering.id}`, []);
|
|
51
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`, offering);
|
|
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: ':offeringId',
|
|
71
65
|
element: <TeacherDashboardTrainingLoader />,
|
|
72
66
|
},
|
|
73
67
|
],
|
|
74
68
|
{
|
|
75
|
-
initialEntries: [`/${
|
|
69
|
+
initialEntries: [`/${offering.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; // offerings 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/offerings/${offering.id}/`);
|
|
97
89
|
|
|
98
90
|
// main titles
|
|
99
91
|
expect(
|
|
@@ -103,27 +95,27 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
103
95
|
).toBeInTheDocument();
|
|
104
96
|
|
|
105
97
|
expect(
|
|
106
|
-
screen.getAllByRole('heading', { name: capitalize(
|
|
98
|
+
screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
|
|
107
99
|
).toHaveLength(2);
|
|
108
100
|
|
|
109
|
-
const nbCourseRun =
|
|
101
|
+
const nbCourseRun = offering.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 offering with organization id if there is one in the path', async () => {
|
|
117
109
|
const organization = OrganizationFactory().one();
|
|
118
|
-
const
|
|
110
|
+
const offering = OfferingFactory({
|
|
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}/offerings/${offering.id}/`,
|
|
115
|
+
offering,
|
|
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/?offering_id=${offering.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/:offeringId',
|
|
142
134
|
element: <TeacherDashboardTrainingLoader />,
|
|
143
135
|
},
|
|
144
136
|
],
|
|
145
137
|
{
|
|
146
|
-
initialEntries: [`/${organization.id}/${
|
|
138
|
+
initialEntries: [`/${organization.id}/${offering.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; // offerings 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}/offerings/${offering.id}/`,
|
|
167
159
|
);
|
|
168
160
|
|
|
169
161
|
// main titles
|
|
@@ -174,10 +166,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
174
166
|
).toBeInTheDocument();
|
|
175
167
|
|
|
176
168
|
expect(
|
|
177
|
-
screen.getAllByRole('heading', { name: capitalize(
|
|
169
|
+
screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
|
|
178
170
|
).toHaveLength(2);
|
|
179
171
|
|
|
180
|
-
const nbCourseRun =
|
|
172
|
+
const nbCourseRun = offering.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 { Offering } from 'types/Joanie';
|
|
10
10
|
|
|
11
11
|
const messages = defineMessages({
|
|
12
|
-
|
|
12
|
+
errorNoOffering: {
|
|
13
13
|
defaultMessage: "This product doesn't exist",
|
|
14
|
-
description: 'Message displayed when requested
|
|
15
|
-
id: 'components.TeacherDashboardTraining.
|
|
14
|
+
description: 'Message displayed when requested offering is not found',
|
|
15
|
+
id: 'components.TeacherDashboardTraining.errorNoOffering',
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
interface TeacherDashboardTrainingProps {
|
|
20
|
-
|
|
20
|
+
offering: Offering;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const TeacherDashboardTraining = ({
|
|
24
|
-
courseProductRelation,
|
|
25
|
-
}: TeacherDashboardTrainingProps) => {
|
|
23
|
+
export const TeacherDashboardTraining = ({ offering }: TeacherDashboardTrainingProps) => {
|
|
26
24
|
const intl = useIntl();
|
|
27
|
-
return
|
|
25
|
+
return offering ? (
|
|
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(offering.product.title)}</h2>
|
|
37
33
|
</div>
|
|
38
|
-
{
|
|
39
|
-
<div className="dashboard__quote">{
|
|
34
|
+
{offering.product.description && (
|
|
35
|
+
<div className="dashboard__quote">{offering.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
|
+
{offering.product.target_courses.map((course) => (
|
|
48
44
|
<DashboardLayout.Section key={`course_target_${course.code}`}>
|
|
49
45
|
<DashboardCard
|
|
50
46
|
className="icon-arrow-right-rounded"
|
|
@@ -67,7 +63,7 @@ export const TeacherDashboardTraining = ({
|
|
|
67
63
|
</div>
|
|
68
64
|
) : (
|
|
69
65
|
<Banner
|
|
70
|
-
message={intl.formatMessage(messages.
|
|
66
|
+
message={intl.formatMessage(messages.errorNoOffering)}
|
|
71
67
|
type={BannerType.ERROR}
|
|
72
68
|
rounded
|
|
73
69
|
/>
|
package/js/types/Course.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface Course extends Resource {
|
|
|
47
47
|
certificate_price: Nullable<number>;
|
|
48
48
|
price: Nullable<number>;
|
|
49
49
|
price_currency: string;
|
|
50
|
+
discounted_price: Nullable<number>;
|
|
51
|
+
discount: Nullable<string>;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export function isRichieCourse(course: Course | JoanieCourse): course is Course {
|
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
|
+
offering_id?: Offering['id'];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface ContractDefinition {
|
|
@@ -149,6 +149,8 @@ export interface Product {
|
|
|
149
149
|
state: CourseState;
|
|
150
150
|
instructions: Nullable<string>;
|
|
151
151
|
contract_definition?: ContractDefinition;
|
|
152
|
+
discounted_price: Nullable<number>;
|
|
153
|
+
discount: Nullable<string>;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
export interface CredentialProduct extends Product {
|
|
@@ -174,7 +176,7 @@ export interface DefinitionResourcesProduct {
|
|
|
174
176
|
contract_definition_id: Nullable<ContractDefinition['id']>;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
export interface
|
|
179
|
+
export interface OfferingLight {
|
|
178
180
|
id: string;
|
|
179
181
|
course: CourseLight;
|
|
180
182
|
organizations: Organization[];
|
|
@@ -182,13 +184,25 @@ export interface CourseProductRelationLight {
|
|
|
182
184
|
created_on: string;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
export interface
|
|
186
|
-
|
|
187
|
+
export interface OfferingRule {
|
|
188
|
+
discounted_price: Nullable<number>;
|
|
189
|
+
discount_rate: Nullable<number>;
|
|
190
|
+
discount_amount: Nullable<number>;
|
|
191
|
+
discount_start: Nullable<string>;
|
|
192
|
+
discount_end: Nullable<string>;
|
|
193
|
+
description: Nullable<string>;
|
|
194
|
+
nb_available_seats: Nullable<number>;
|
|
195
|
+
has_seat_limit: boolean;
|
|
196
|
+
has_seats_left: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface Offering extends OfferingLight {
|
|
187
200
|
is_withdrawable: boolean;
|
|
201
|
+
rules: OfferingRule;
|
|
188
202
|
}
|
|
189
|
-
export function
|
|
190
|
-
entity: CourseListItem |
|
|
191
|
-
): entity is
|
|
203
|
+
export function isOffering(
|
|
204
|
+
entity: CourseListItem | OfferingLight | RichieCourse,
|
|
205
|
+
): entity is OfferingLight {
|
|
192
206
|
return 'course' in entity && 'product' in entity;
|
|
193
207
|
}
|
|
194
208
|
|
|
@@ -233,7 +247,7 @@ export interface Enrollment {
|
|
|
233
247
|
was_created_by_order: boolean;
|
|
234
248
|
created_on: string;
|
|
235
249
|
orders: OrderEnrollment[];
|
|
236
|
-
|
|
250
|
+
offerings: Offering[];
|
|
237
251
|
certificate_id: Nullable<string>;
|
|
238
252
|
}
|
|
239
253
|
export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
|
|
@@ -247,7 +261,7 @@ export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj
|
|
|
247
261
|
'was_created_by_order' in obj &&
|
|
248
262
|
'created_on' in obj &&
|
|
249
263
|
'orders' in obj &&
|
|
250
|
-
'
|
|
264
|
+
'offerings' in obj &&
|
|
251
265
|
'certificate_id' in obj
|
|
252
266
|
);
|
|
253
267
|
};
|
|
@@ -325,7 +339,6 @@ export interface Order {
|
|
|
325
339
|
enrollment: Nullable<EnrollmentLight>;
|
|
326
340
|
organization_id: Organization['id'];
|
|
327
341
|
organization: Organization;
|
|
328
|
-
order_group_id?: OrderGroup['id'];
|
|
329
342
|
payment_schedule?: PaymentSchedule;
|
|
330
343
|
credit_card_id?: CreditCard['id'];
|
|
331
344
|
}
|
|
@@ -398,18 +411,11 @@ export interface NestedCourseOrder {
|
|
|
398
411
|
|
|
399
412
|
export interface CourseOrderResourceQuery extends PaginatedResourceQuery {
|
|
400
413
|
course_id?: CourseListItem['id'];
|
|
401
|
-
|
|
414
|
+
offering_id?: Offering['id'];
|
|
402
415
|
organization_id?: Organization['id'];
|
|
403
416
|
product_id?: Product['id'];
|
|
404
417
|
}
|
|
405
418
|
|
|
406
|
-
export interface OrderGroup {
|
|
407
|
-
id: string;
|
|
408
|
-
is_active: boolean;
|
|
409
|
-
nb_seats: number;
|
|
410
|
-
nb_available_seats: number;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
419
|
export enum CreditCardBrand {
|
|
414
420
|
MASTERCARD = 'mastercard',
|
|
415
421
|
MAESTRO = 'maestro',
|
|
@@ -481,7 +487,6 @@ export interface AddressCreationPayload extends Omit<Address, 'id' | 'is_main'>
|
|
|
481
487
|
|
|
482
488
|
interface AbstractOrderProductCreationPayload {
|
|
483
489
|
product_id: Product['id'];
|
|
484
|
-
order_group_id?: OrderGroup['id'];
|
|
485
490
|
billing_address: Omit<Address, 'id' | 'is_main'>;
|
|
486
491
|
has_waived_withdrawal_right: boolean;
|
|
487
492
|
}
|
|
@@ -541,8 +546,8 @@ export interface CourseProductQueryFilters extends ResourcesQuery {
|
|
|
541
546
|
id?: Product['id'];
|
|
542
547
|
course_id?: CourseListItem['id'];
|
|
543
548
|
}
|
|
544
|
-
export interface
|
|
545
|
-
id?:
|
|
549
|
+
export interface OfferingQueryFilters extends PaginatedResourceQuery {
|
|
550
|
+
id?: Offering['id'];
|
|
546
551
|
organization_id?: Organization['id'];
|
|
547
552
|
product_type?: ProductType;
|
|
548
553
|
query?: string;
|
|
@@ -555,7 +560,7 @@ export enum ContractState {
|
|
|
555
560
|
}
|
|
556
561
|
export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
557
562
|
organization_id?: Organization['id'];
|
|
558
|
-
|
|
563
|
+
offering_id?: Offering['id'];
|
|
559
564
|
contract_ids?: Contract['id'][];
|
|
560
565
|
signature_state?: ContractState;
|
|
561
566
|
}
|
|
@@ -563,7 +568,7 @@ export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
|
563
568
|
export interface OrganizationContractSignatureLinksFilters {
|
|
564
569
|
contracts_ids?: string[];
|
|
565
570
|
organization_id: Organization['id'];
|
|
566
|
-
|
|
571
|
+
offering_ids?: Offering['id'][];
|
|
567
572
|
}
|
|
568
573
|
|
|
569
574
|
export interface ContractInvitationLinkResponse {
|
|
@@ -657,10 +662,10 @@ interface APIUser {
|
|
|
657
662
|
check: (id: string) => Promise<Response>;
|
|
658
663
|
create: ({
|
|
659
664
|
organization_id,
|
|
660
|
-
|
|
665
|
+
offering_id,
|
|
661
666
|
}: {
|
|
662
667
|
organization_id?: Organization['id'];
|
|
663
|
-
|
|
668
|
+
offering_id?: Offering['id'];
|
|
664
669
|
}) => Promise<{ url: string }>;
|
|
665
670
|
get: (id: string) => Promise<File>;
|
|
666
671
|
};
|
|
@@ -676,7 +681,7 @@ export interface API {
|
|
|
676
681
|
? Promise<Nullable<CourseListItem>>
|
|
677
682
|
: Promise<PaginatedResponse<CourseListItem>>;
|
|
678
683
|
products: {
|
|
679
|
-
get(filters?: CourseProductQueryFilters): Promise<Nullable<
|
|
684
|
+
get(filters?: CourseProductQueryFilters): Promise<Nullable<Offering>>;
|
|
680
685
|
paymentSchedule: {
|
|
681
686
|
get(filters?: CourseProductQueryFilters): Promise<Nullable<PaymentSchedule>>;
|
|
682
687
|
};
|
|
@@ -709,12 +714,12 @@ export interface API {
|
|
|
709
714
|
filters?: CourseRunFilters,
|
|
710
715
|
): CourseRunFilters extends { id: string } ? Promise<Nullable<CourseRun>> : Promise<CourseRun>;
|
|
711
716
|
};
|
|
712
|
-
|
|
717
|
+
offerings: {
|
|
713
718
|
get<Filters extends PaginatedResourceQuery = PaginatedResourceQuery>(
|
|
714
719
|
filters?: Filters,
|
|
715
720
|
): Filters extends { id: string }
|
|
716
|
-
? Promise<Nullable<
|
|
717
|
-
: Promise<PaginatedResponse<
|
|
721
|
+
? Promise<Nullable<Offering>>
|
|
722
|
+
: Promise<PaginatedResponse<OfferingLight>>;
|
|
718
723
|
};
|
|
719
724
|
contractDefinitions: {
|
|
720
725
|
previewTemplate(id: string): Promise<File>;
|
package/js/types/index.ts
CHANGED
|
@@ -35,11 +35,13 @@ export interface CourseRun {
|
|
|
35
35
|
title?: string;
|
|
36
36
|
snapshot?: string;
|
|
37
37
|
display_mode: CourseRunDisplayMode;
|
|
38
|
-
price?: number
|
|
38
|
+
price?: Nullable<number>;
|
|
39
39
|
price_currency?: string;
|
|
40
40
|
offer?: string;
|
|
41
|
-
certificate_price?: number
|
|
41
|
+
certificate_price?: Nullable<number>;
|
|
42
42
|
certificate_offer?: string;
|
|
43
|
+
discounted_price: Nullable<number>;
|
|
44
|
+
discount: Nullable<string>;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export enum Priority {
|
|
@@ -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
|
+
Offering,
|
|
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
|
+
offerings: OfferingFactory().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(),
|
|
@@ -206,6 +205,8 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
|
|
|
206
205
|
remaining_order_count: faker.number.int({ min: 1, max: 100 }),
|
|
207
206
|
state: CourseStateFactory().one(),
|
|
208
207
|
instructions: null,
|
|
208
|
+
discounted_price: null,
|
|
209
|
+
discount: null,
|
|
209
210
|
};
|
|
210
211
|
});
|
|
211
212
|
|
|
@@ -294,25 +295,6 @@ export const CourseLightFactory = factory((): CourseLight => {
|
|
|
294
295
|
};
|
|
295
296
|
});
|
|
296
297
|
|
|
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
298
|
export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
317
299
|
return {
|
|
318
300
|
id: faker.string.uuid(),
|
|
@@ -329,15 +311,25 @@ export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
|
329
311
|
};
|
|
330
312
|
});
|
|
331
313
|
|
|
332
|
-
export const
|
|
314
|
+
export const OfferingFactory = factory((): Offering => {
|
|
333
315
|
return {
|
|
334
316
|
id: faker.string.uuid(),
|
|
335
317
|
created_on: faker.date.past().toISOString(),
|
|
336
318
|
course: CourseFactory().one(),
|
|
337
319
|
product: ProductFactory().one(),
|
|
338
320
|
organizations: OrganizationFactory().many(1),
|
|
339
|
-
order_groups: [],
|
|
340
321
|
is_withdrawable: true,
|
|
322
|
+
rules: {
|
|
323
|
+
discounted_price: null,
|
|
324
|
+
discount_rate: null,
|
|
325
|
+
discount_amount: null,
|
|
326
|
+
discount_start: null,
|
|
327
|
+
discount_end: null,
|
|
328
|
+
description: null,
|
|
329
|
+
nb_available_seats: null,
|
|
330
|
+
has_seat_limit: false,
|
|
331
|
+
has_seats_left: true,
|
|
332
|
+
},
|
|
341
333
|
};
|
|
342
334
|
});
|
|
343
335
|
|
|
@@ -60,11 +60,11 @@ export const CourseRunFactory = factory<CourseRun>(() => {
|
|
|
60
60
|
certificateOfferValues[Math.floor(Math.random() * certificateOfferValues.length)];
|
|
61
61
|
const currency = faker.finance.currency().code;
|
|
62
62
|
const price = [OfferType.FREE, OfferType.PARTIALLY_FREE].includes(offer)
|
|
63
|
-
?
|
|
63
|
+
? null
|
|
64
64
|
: parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
|
|
65
65
|
const certificatePrice =
|
|
66
66
|
certificateOffer === OfferType.FREE
|
|
67
|
-
?
|
|
67
|
+
? null
|
|
68
68
|
: parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
|
|
69
69
|
return {
|
|
70
70
|
id: faker.number.int(),
|
|
@@ -83,6 +83,8 @@ export const CourseRunFactory = factory<CourseRun>(() => {
|
|
|
83
83
|
offer,
|
|
84
84
|
certificate_price: certificatePrice,
|
|
85
85
|
certificate_offer: certificateOffer,
|
|
86
|
+
discounted_price: null,
|
|
87
|
+
discount: null,
|
|
86
88
|
};
|
|
87
89
|
});
|
|
88
90
|
|
|
@@ -244,5 +246,7 @@ export const CourseLightFactory = factory<Course>(() => {
|
|
|
244
246
|
certificate_price: null,
|
|
245
247
|
price: null,
|
|
246
248
|
price_currency: 'EUR',
|
|
249
|
+
discounted_price: null,
|
|
250
|
+
discount: null,
|
|
247
251
|
};
|
|
248
252
|
});
|
|
@@ -3,14 +3,14 @@ import { CredentialOrder } from 'types/Joanie';
|
|
|
3
3
|
import {
|
|
4
4
|
ContractDefinitionFactory,
|
|
5
5
|
CourseFactory,
|
|
6
|
-
|
|
6
|
+
OfferingFactory,
|
|
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 offering = OfferingFactory({
|
|
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
|
+
offering,
|
|
26
26
|
);
|
|
27
|
-
return
|
|
27
|
+
return offering;
|
|
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.offerings.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.offerings[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: offering } = useCourseProduct({
|
|
71
71
|
product_id: order.product_id,
|
|
72
72
|
course_id: course.code,
|
|
73
73
|
});
|
|
74
|
-
const { product } =
|
|
74
|
+
const { product } = offering || {};
|
|
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(offering?.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 offering 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: offering, 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={offering.product as CredentialProduct}
|
|
122
|
+
course={offering.course}
|
|
123
|
+
isWithdrawable={offering.is_withdrawable}
|
|
124
124
|
/>
|
|
125
125
|
</>
|
|
126
126
|
);
|