richie-education 3.2.2-dev36 → 3.2.2-dev38

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.
@@ -21,7 +21,7 @@ const messages = defineMessages({
21
21
  description: {
22
22
  id: 'components.SaleTunnel.Information.description',
23
23
  description: 'Helper text explaining that the information will be used for billing',
24
- defaultMessage: 'Those information will be used for billing',
24
+ defaultMessage: 'This information will be used for billing',
25
25
  },
26
26
  purchaseTypeTitle: {
27
27
  id: 'components.SaleTunnel.Information.purchaseTypeTitle',
@@ -36,12 +36,12 @@ const messages = defineMessages({
36
36
  purchaseTypeOptionSingle: {
37
37
  id: 'components.SaleTunnel.Information.purchaseTypeOptionSingle',
38
38
  description: 'Option label for selecting a single purchase (B2C)',
39
- defaultMessage: 'Single purchase (B2C)',
39
+ defaultMessage: 'I am purchasing as an individual',
40
40
  },
41
41
  purchaseTypeOptionGroup: {
42
42
  id: 'components.SaleTunnel.Information.purchaseTypeOptionGroup',
43
43
  description: 'Option label for selecting a group purchase (B2B)',
44
- defaultMessage: 'Group purchase (B2B)',
44
+ defaultMessage: 'I am purchasing on behalf of an organization',
45
45
  },
46
46
  stepCompany: {
47
47
  id: 'components.SaleTunnel.BatchOrderForm.stepCompany',
@@ -21,7 +21,7 @@ const messages = defineMessages({
21
21
  description: {
22
22
  id: 'components.SaleTunnel.Information.description',
23
23
  description: 'Description of the information section',
24
- defaultMessage: 'Those information will be used for billing',
24
+ defaultMessage: 'This information will be used for billing',
25
25
  },
26
26
  paymentSchedule: {
27
27
  id: 'components.SaleTunnel.Information.paymentSchedule',
@@ -38,7 +38,7 @@ const messages = defineMessages({
38
38
  id: 'components.SaleTunnel.BatchOrderForm.stepAdminTitle',
39
39
  description:
40
40
  'Title of the section with details about the person responsible for admin follow-up',
41
- defaultMessage: 'Responsible for the administrative follow-up',
41
+ defaultMessage: 'Administrative contact',
42
42
  },
43
43
  stepSignatoryTitle: {
44
44
  id: 'components.SaleTunnel.BatchOrderForm.stepSignatoryTitle',
@@ -49,17 +49,17 @@ const messages = defineMessages({
49
49
  stepBillingTitle: {
50
50
  id: 'components.SaleTunnel.BatchOrderForm.stepBillingTitle',
51
51
  description: 'Title of the section with billing details',
52
- defaultMessage: 'Billing informations',
52
+ defaultMessage: 'Billing details',
53
53
  },
54
54
  stepParticipantsTitle: {
55
55
  id: 'components.SaleTunnel.BatchOrderForm.stepParticipantsTitle',
56
56
  description: 'Title of the section to enter the number of registrations/participants',
57
- defaultMessage: 'How many registrations ?',
57
+ defaultMessage: 'Number of registrations',
58
58
  },
59
59
  stepFinancingTitle: {
60
60
  id: 'components.SaleTunnel.BatchOrderForm.stepFinancingTitle',
61
61
  description: 'Title of the section to select the payment plan of the course',
62
- defaultMessage: 'Payment plan of the course',
62
+ defaultMessage: 'Ordering method',
63
63
  },
64
64
  companyName: {
65
65
  id: 'batchOrder.companyName',
@@ -70,7 +70,7 @@ const messages = defineMessages({
70
70
  id: 'batchOrder.identificationNumber',
71
71
  description:
72
72
  'Label for the field asking the company identification number (eg. SIRET in France)',
73
- defaultMessage: 'Identification number (SIRET for french company)',
73
+ defaultMessage: 'Registration number (SIRET for French companies)',
74
74
  },
75
75
  vatNumber: {
76
76
  id: 'batchOrder.vatNumber',
@@ -85,7 +85,7 @@ const messages = defineMessages({
85
85
  postCode: {
86
86
  id: 'batchOrder.postCode',
87
87
  description: 'Label for the field asking the postal code',
88
- defaultMessage: 'Post code',
88
+ defaultMessage: 'Postal code',
89
89
  },
90
90
  city: {
91
91
  id: 'batchOrder.city',
@@ -120,42 +120,42 @@ const messages = defineMessages({
120
120
  phone: {
121
121
  id: 'batchOrder.phone',
122
122
  description: 'Label for the field asking the phone number of the contact person',
123
- defaultMessage: 'Phone',
123
+ defaultMessage: 'Phone number',
124
124
  },
125
125
  checkBilling: {
126
126
  id: 'components.SaleTunnel.BatchOrderForm.checkBilling',
127
127
  description: 'Checkbox label to indicate using alternative billing information',
128
- defaultMessage: 'Use other informations for billing',
128
+ defaultMessage: 'Use different billing information',
129
129
  },
130
130
  contactName: {
131
131
  id: 'batchOrder.contactName',
132
132
  description: 'Label for the field asking the billing contact name',
133
- defaultMessage: 'Name of the contact',
133
+ defaultMessage: 'Contact name',
134
134
  },
135
135
  contactEmail: {
136
136
  id: 'batchOrder.contactEmail',
137
137
  description: 'Label for the field asking the billing contact email',
138
- defaultMessage: 'Email of the contact',
138
+ defaultMessage: 'Contact email',
139
139
  },
140
140
  nbSeats: {
141
141
  id: 'batchOrder.nbSeats',
142
142
  description: 'Label for the field asking the number of participants/seats',
143
- defaultMessage: 'How many participants ?',
143
+ defaultMessage: 'Number of participants to register',
144
144
  },
145
145
  cardPayment: {
146
146
  id: 'batchOrder.cardPayment',
147
147
  description: 'Option label for selecting credit card payment',
148
- defaultMessage: 'Payment by credit card',
148
+ defaultMessage: 'Credit card',
149
149
  },
150
150
  bankTransfer: {
151
151
  id: 'batchOrder.bankTransfer',
152
152
  description: 'Option label for selecting bank transfer payment',
153
- defaultMessage: 'Payment by bank transfer',
153
+ defaultMessage: 'Bank transfer',
154
154
  },
155
155
  purchaseOrder: {
156
156
  id: 'batchOrder.purchaseOrder',
157
157
  description: 'Option label for selecting payment via purchase order',
158
- defaultMessage: 'Payment with purchase order',
158
+ defaultMessage: 'Purchase order',
159
159
  },
160
160
  withoutOrderForm: {
161
161
  id: 'batchOrder.withoutOrderForm',
@@ -191,7 +191,7 @@ const messages = defineMessages({
191
191
  participatingOrganisations: {
192
192
  id: 'batchOrder.participatingOrganisations',
193
193
  description: 'Label for the field listing other participating organisations',
194
- defaultMessage: 'Participating organisations',
194
+ defaultMessage: 'Participating organizations',
195
195
  },
196
196
  });
197
197
 
@@ -19,12 +19,12 @@ const messages = defineMessages({
19
19
  purchaseTypeOptionSingle: {
20
20
  id: 'components.SaleTunnel.Information.purchaseTypeOptionSingle',
21
21
  description: 'Label for B2C option',
22
- defaultMessage: 'Single purchase (B2C)',
22
+ defaultMessage: 'I am purchasing as an individual',
23
23
  },
24
24
  purchaseTypeOptionGroup: {
25
25
  id: 'components.SaleTunnel.Information.purchaseTypeOptionGroup',
26
26
  description: 'Label for B2C option',
27
- defaultMessage: 'Group purchase (B2B)',
27
+ defaultMessage: 'I am purchasing on behalf of an organization',
28
28
  },
29
29
  });
30
30
 
@@ -152,13 +152,13 @@ describe('SaleTunnel', () => {
152
152
  expectMenuToBeClosed(menu);
153
153
  await user.click(formTypeSelect);
154
154
  expectMenuToBeOpen(menu);
155
- await user.click(screen.getByText('Group purchase (B2B)'));
155
+ await user.click(screen.getByText('I am purchasing on behalf of an organization'));
156
156
 
157
157
  // Company step
158
158
  const $companyName = await screen.findByRole('textbox', { name: 'Company name' });
159
- const $idNumber = screen.getByRole('textbox', { name: /Identification number/ });
159
+ const $idNumber = screen.getByRole('textbox', { name: /Registration number/ });
160
160
  const $address = screen.getByRole('textbox', { name: 'Address' });
161
- const $postCode = screen.getByRole('textbox', { name: 'Post code' });
161
+ const $postCode = screen.getByRole('textbox', { name: 'Postal code' });
162
162
  const $city = screen.getByRole('textbox', { name: 'City' });
163
163
  const $country = screen.getByRole('combobox', { name: 'Country' });
164
164
 
@@ -183,7 +183,7 @@ describe('SaleTunnel', () => {
183
183
  const $firstName = screen.getByRole('textbox', { name: 'First name' });
184
184
  const $role = screen.getByRole('textbox', { name: 'Role' });
185
185
  const $email = screen.getByRole('textbox', { name: 'Email' });
186
- const $phone = screen.getByRole('textbox', { name: 'Phone' });
186
+ const $phone = screen.getByRole('textbox', { name: 'Phone number' });
187
187
 
188
188
  await user.type($lastName, 'Doe');
189
189
  await user.type($firstName, 'John');
@@ -200,7 +200,7 @@ describe('SaleTunnel', () => {
200
200
  const $signatoryFirstName = screen.getByRole('textbox', { name: 'First name' });
201
201
  const $signatoryRole = screen.getByRole('textbox', { name: 'Role' });
202
202
  const $signatoryEmail = screen.getByRole('textbox', { name: 'Email' });
203
- const $signatoryPhone = screen.getByRole('textbox', { name: 'Phone' });
203
+ const $signatoryPhone = screen.getByRole('textbox', { name: 'Phone number' });
204
204
 
205
205
  await user.type($signatoryLastName, 'Doe');
206
206
  await user.type($signatoryFirstName, 'John');
@@ -210,7 +210,7 @@ describe('SaleTunnel', () => {
210
210
 
211
211
  // Participants step
212
212
  await user.click(screen.getByRole('button', { name: 'Next' }));
213
- const $nbParticipants = await screen.findByLabelText('How many participants ?');
213
+ const $nbParticipants = await screen.findByLabelText('Number of participants to register');
214
214
  await user.type($nbParticipants, '13');
215
215
  expect($nbParticipants).toHaveValue(13);
216
216
 
@@ -429,13 +429,13 @@ describe('SaleTunnel', () => {
429
429
  expectMenuToBeClosed(menu);
430
430
  await user.click(formTypeSelect);
431
431
  expectMenuToBeOpen(menu);
432
- await user.click(screen.getByText('Group purchase (B2B)'));
432
+ await user.click(screen.getByText('I am purchasing on behalf of an organization'));
433
433
 
434
434
  // Company step
435
435
  const $companyName = await screen.findByRole('textbox', { name: 'Company name' });
436
- const $idNumber = screen.getByRole('textbox', { name: /Identification number/ });
436
+ const $idNumber = screen.getByRole('textbox', { name: /Registration number/ });
437
437
  const $address = screen.getByRole('textbox', { name: 'Address' });
438
- const $postCode = screen.getByRole('textbox', { name: 'Post code' });
438
+ const $postCode = screen.getByRole('textbox', { name: 'Postal code' });
439
439
  const $city = screen.getByRole('textbox', { name: 'City' });
440
440
  const $country = screen.getByRole('combobox', { name: 'Country' });
441
441
 
@@ -460,7 +460,7 @@ describe('SaleTunnel', () => {
460
460
  const $firstName = screen.getByRole('textbox', { name: 'First name' });
461
461
  const $role = screen.getByRole('textbox', { name: 'Role' });
462
462
  const $email = screen.getByRole('textbox', { name: 'Email' });
463
- const $phone = screen.getByRole('textbox', { name: 'Phone' });
463
+ const $phone = screen.getByRole('textbox', { name: 'Phone number' });
464
464
 
465
465
  await user.type($lastName, 'Doe');
466
466
  await user.type($firstName, 'John');
@@ -477,7 +477,7 @@ describe('SaleTunnel', () => {
477
477
  const $signatoryFirstName = screen.getByRole('textbox', { name: 'First name' });
478
478
  const $signatoryRole = screen.getByRole('textbox', { name: 'Role' });
479
479
  const $signatoryEmail = screen.getByRole('textbox', { name: 'Email' });
480
- const $signatoryPhone = screen.getByRole('textbox', { name: 'Phone' });
480
+ const $signatoryPhone = screen.getByRole('textbox', { name: 'Phone number' });
481
481
 
482
482
  await user.type($signatoryLastName, 'Doe');
483
483
  await user.type($signatoryFirstName, 'John');
@@ -487,7 +487,7 @@ describe('SaleTunnel', () => {
487
487
 
488
488
  // Participants step
489
489
  await user.click(screen.getByRole('button', { name: 'Next' }));
490
- const $nbParticipants = await screen.findByLabelText('How many participants ?');
490
+ const $nbParticipants = await screen.findByLabelText('Number of participants to register');
491
491
  await user.type($nbParticipants, '13');
492
492
  expect($nbParticipants).toHaveValue(13);
493
493
 
@@ -772,7 +772,7 @@ describe.each([
772
772
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
773
773
  });
774
774
  expect(
775
- await screen.queryByText('Those information will be used for billing'),
775
+ await screen.queryByText('This information will be used for billing'),
776
776
  ).toBeInTheDocument();
777
777
 
778
778
  await user.type(screen.getByLabelText('Voucher code'), 'DISCOUNT100');
@@ -781,7 +781,7 @@ describe.each([
781
781
  expect(await screen.findByText('Discount applied')).toBeInTheDocument();
782
782
  await waitFor(async () =>
783
783
  expect(
784
- await screen.queryByText('Those information will be used for billing'),
784
+ await screen.queryByText('This information will be used for billing'),
785
785
  ).not.toBeInTheDocument(),
786
786
  );
787
787
  expect(await screen.queryByTestId('withdraw-right-checkbox')).not.toBeInTheDocument();
@@ -201,4 +201,133 @@ describe('full process for the organization quotes dashboard', () => {
201
201
  expect(processPaymentButton).toBeVisible();
202
202
  expect(processPaymentButton).toBeDisabled();
203
203
  });
204
+
205
+ it('should work with purchase order payment method workflow', async () => {
206
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/`, []);
207
+ const organization = OrganizationFactory({
208
+ abilities: {
209
+ can_submit_for_signature_batch_order: true,
210
+ confirm_bank_transfer: true,
211
+ confirm_quote: true,
212
+ download_quote: true,
213
+ sign_contracts: true,
214
+ },
215
+ }).one();
216
+ fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/1/', organization);
217
+
218
+ const quoteQuoted = OrganizationQuoteFactory({
219
+ batch_order: {
220
+ state: BatchOrderState.QUOTED,
221
+ payment_method: PaymentMethod.PURCHASE_ORDER,
222
+ available_actions: { next_action: 'confirm_quote' },
223
+ },
224
+ organization_signed_on: undefined,
225
+ }).one();
226
+
227
+ const quotePurchaseOrder = OrganizationQuoteFactory({
228
+ id: quoteQuoted.id,
229
+ batch_order: {
230
+ state: BatchOrderState.QUOTED,
231
+ payment_method: PaymentMethod.PURCHASE_ORDER,
232
+ available_actions: { next_action: 'confirm_purchase_order' },
233
+ },
234
+ }).one();
235
+
236
+ const quoteSendForSign = OrganizationQuoteFactory({
237
+ id: quoteQuoted.id,
238
+ batch_order: {
239
+ state: BatchOrderState.TO_SIGN,
240
+ payment_method: PaymentMethod.PURCHASE_ORDER,
241
+ contract_submitted: false,
242
+ available_actions: { next_action: 'submit_for_signature' },
243
+ },
244
+ }).one();
245
+
246
+ const quoteCompleted = OrganizationQuoteFactory({
247
+ id: quoteQuoted.id,
248
+ batch_order: {
249
+ state: BatchOrderState.COMPLETED,
250
+ payment_method: PaymentMethod.PURCHASE_ORDER,
251
+ },
252
+ }).one();
253
+
254
+ const quotesResponses = [
255
+ { results: [quoteQuoted], count: 1, previous: null, next: null },
256
+ { results: [quotePurchaseOrder], count: 1, previous: null, next: null },
257
+ { results: [quoteSendForSign], count: 1, previous: null, next: null },
258
+ { results: [quoteCompleted], count: 1, previous: null, next: null },
259
+ ];
260
+
261
+ fetchMock.get(
262
+ `https://joanie.endpoint/api/v1.0/organizations/1/quotes/?page=1&page_size=10`,
263
+ () => quotesResponses.shift(),
264
+ );
265
+
266
+ fetchMock.patch(`https://joanie.endpoint/api/v1.0/organizations/1/confirm-quote/`, 200);
267
+ fetchMock.patch(
268
+ `https://joanie.endpoint/api/v1.0/organizations/1/confirm-purchase-order/`,
269
+ 200,
270
+ );
271
+ fetchMock.post(
272
+ `https://joanie.endpoint/api/v1.0/organizations/1/submit-for-signature-batch-order/`,
273
+ 200,
274
+ );
275
+
276
+ render(<TeacherDashboardOrganizationQuotes />, {
277
+ routerOptions: {
278
+ path: '/organizations/:organizationId/quotes',
279
+ initialEntries: ['/organizations/1/quotes'],
280
+ },
281
+ });
282
+
283
+ await expectNoSpinner();
284
+
285
+ // First step: confirm quote
286
+ const confirmQuoteButton = await screen.findByRole('button', { name: /confirm quote/i });
287
+ expect(confirmQuoteButton).toBeVisible();
288
+ await user.click(confirmQuoteButton);
289
+ await screen.findByText(/total amount/i);
290
+ await user.type(screen.getByLabelText(/total amount/i), '1000');
291
+ await user.click(screen.getByRole('button', { name: /confirm quote/i }));
292
+
293
+ // Second step: confirm purchase order with reference via modal
294
+ const confirmPurchaseOrderButton = await screen.findByRole('button', {
295
+ name: /confirm receipt of purchase order/i,
296
+ });
297
+ expect(confirmPurchaseOrderButton).toBeVisible();
298
+ await user.click(confirmPurchaseOrderButton);
299
+
300
+ // Modal should open with purchase order reference input
301
+ const modalTitle = await screen.findByText(/confirm purchase order/i);
302
+ expect(modalTitle).toBeInTheDocument();
303
+
304
+ const referenceInput = screen.getByLabelText(/purchase order reference/i);
305
+ expect(referenceInput).toBeInTheDocument();
306
+
307
+ // Type reference and confirm
308
+ await user.type(referenceInput, 'this-is-a-reference');
309
+ await user.click(screen.getByRole('button', { name: /confirm receipt of purchase order/i }));
310
+
311
+ // Third step: send for signature
312
+ const sendForSignatureButton = await screen.findByRole('button', {
313
+ name: /send contract for signature/i,
314
+ });
315
+ expect(sendForSignatureButton).toBeVisible();
316
+ await user.click(sendForSignatureButton);
317
+
318
+ // Verify completed state
319
+ cleanup();
320
+ render(<TeacherDashboardOrganizationQuotes />, {
321
+ routerOptions: {
322
+ path: '/organizations/:organizationId/quotes',
323
+ initialEntries: ['/organizations/1/quotes'],
324
+ },
325
+ });
326
+ await expectNoSpinner();
327
+
328
+ // No action button should be visible for completed quote
329
+ expect(
330
+ screen.queryByRole('button', { name: /confirm receipt of purchase order/i }),
331
+ ).not.toBeInTheDocument();
332
+ });
204
333
  });
@@ -66,6 +66,16 @@ const messages = defineMessages({
66
66
  id: 'components.OrganizationQuotesTable.confirmPurchaseOrder',
67
67
  description: 'Label for confirming receipt of a purchase order',
68
68
  },
69
+ purchaseOrderModalTitle: {
70
+ defaultMessage: 'Confirm purchase order',
71
+ id: 'components.OrganizationQuotesTable.purchaseOrderModalTitle',
72
+ description: 'Title of the confirm purchase order modal',
73
+ },
74
+ purchaseOrderReferenceLabel: {
75
+ defaultMessage: 'Purchase order reference',
76
+ id: 'components.OrganizationQuotesTable.purchaseOrderReferenceLabel',
77
+ description: 'Label for the purchase order reference input',
78
+ },
69
79
  confirmBank: {
70
80
  defaultMessage: 'Confirm bank transfer',
71
81
  id: 'components.OrganizationQuotesTable.confirmBank',
@@ -249,6 +259,11 @@ const TeacherDashboardOrganizationQuotes = () => {
249
259
  const [amount, setAmount] = useState('');
250
260
  const [isModalOpen, setIsModalOpen] = useState(false);
251
261
 
262
+ const [selectedPurchaseOrderQuote, setSelectedPurchaseOrderQuote] =
263
+ useState<OrganizationQuote | null>(null);
264
+ const [purchaseOrderReference, setPurchaseOrderReference] = useState('');
265
+ const [isPurchaseOrderModalOpen, setIsPurchaseOrderModalOpen] = useState(false);
266
+
252
267
  useEffect(() => {
253
268
  if (meta?.pagination?.count) {
254
269
  pagination.setItemsCount(meta.pagination.count);
@@ -294,11 +309,27 @@ const TeacherDashboardOrganizationQuotes = () => {
294
309
  await invalidate();
295
310
  };
296
311
 
297
- const handleConfirmPurchaseOrder = async (id: string) => {
312
+ const handleOpenPurchaseOrderModal = (quote: OrganizationQuote) => {
313
+ setSelectedPurchaseOrderQuote(quote);
314
+ setIsPurchaseOrderModalOpen(true);
315
+ };
316
+
317
+ const handleCancelPurchaseOrder = () => {
318
+ setIsPurchaseOrderModalOpen(false);
319
+ setPurchaseOrderReference('');
320
+ setSelectedPurchaseOrderQuote(null);
321
+ };
322
+
323
+ const handleConfirmPurchaseOrder = async () => {
324
+ if (!selectedPurchaseOrderQuote) return;
298
325
  await confirmPurchaseOrder({
299
326
  organization_id: routeOrganizationId,
300
- payload: { quote_id: id },
327
+ payload: {
328
+ quote_id: selectedPurchaseOrderQuote.id,
329
+ purchase_order_reference: purchaseOrderReference,
330
+ },
301
331
  });
332
+ handleCancelPurchaseOrder();
302
333
  await invalidate();
303
334
  };
304
335
 
@@ -363,7 +394,7 @@ const TeacherDashboardOrganizationQuotes = () => {
363
394
  <Button
364
395
  size="small"
365
396
  className="ml-2"
366
- onClick={() => handleConfirmPurchaseOrder(quote.id)}
397
+ onClick={() => handleOpenPurchaseOrderModal(quote)}
367
398
  icon={<span className="material-icons">description</span>}
368
399
  >
369
400
  {intl.formatMessage(messages.confirmPurchaseOrder)}
@@ -525,6 +556,26 @@ const TeacherDashboardOrganizationQuotes = () => {
525
556
  />
526
557
  </div>
527
558
  </Modal>
559
+ <Modal
560
+ isOpen={isPurchaseOrderModalOpen}
561
+ onClose={handleCancelPurchaseOrder}
562
+ title={intl.formatMessage(messages.purchaseOrderModalTitle)}
563
+ size={ModalSize.MEDIUM}
564
+ actions={
565
+ <Button size="small" onClick={handleConfirmPurchaseOrder}>
566
+ {intl.formatMessage(messages.confirmPurchaseOrder)}
567
+ </Button>
568
+ }
569
+ >
570
+ <div className="dashboard__quote__modal">
571
+ <Input
572
+ type="text"
573
+ label={intl.formatMessage(messages.purchaseOrderReferenceLabel)}
574
+ onChange={(e) => setPurchaseOrderReference(e.target.value)}
575
+ value={purchaseOrderReference}
576
+ />
577
+ </div>
578
+ </Modal>
528
579
  </div>
529
580
  );
530
581
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.2.2-dev36",
3
+ "version": "3.2.2-dev38",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {