@graphcommerce/magento-customer 5.1.1 → 5.2.0-canary.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/components/AccountLatestOrder/AccountLatestOrder.tsx +31 -0
  3. package/components/AccountMenu/AccountMenu.tsx +21 -0
  4. package/components/AccountMenuItem/AccountMenuItem.tsx +105 -0
  5. package/components/AccountOrders/AccountOrders.graphql +13 -0
  6. package/components/AccountOrders/AccountOrders.tsx +88 -0
  7. package/components/AccountSignInUpForm/AccountSignInUpForm.tsx +167 -0
  8. package/components/CreateCustomerAddressForm/CreateCustomerAddressForm.tsx +1 -1
  9. package/components/DeleteCustomerAddressForm/DeleteCustomerAddressForm.tsx +11 -1
  10. package/components/NoOrdersFound/NoOrdersFound.tsx +22 -0
  11. package/components/OrderCard/OrderCard.graphql +18 -0
  12. package/components/OrderCard/OrderCard.tsx +160 -0
  13. package/components/OrderCardItem/OrderCardItem.graphql +4 -0
  14. package/components/OrderCardItem/OrderCardItem.tsx +17 -0
  15. package/components/OrderCardItemImage/OrderCardItemImage.tsx +35 -0
  16. package/components/OrderDetails/OrderDetails.graphql +77 -0
  17. package/components/OrderDetails/OrderDetails.tsx +371 -0
  18. package/components/OrderItem/OrderItem.graphql +14 -0
  19. package/components/OrderItem/OrderItem.tsx +208 -0
  20. package/components/OrderItems/OrderItems.graphql +5 -0
  21. package/components/OrderItems/OrderItems.tsx +127 -0
  22. package/components/OrderStateLabel/OrderStateLabel.graphql +10 -0
  23. package/components/OrderStateLabel/OrderStateLabel.tsx +92 -0
  24. package/components/OrderStateLabel/OrderStateLabelInline.tsx +38 -0
  25. package/components/TrackingLink/TrackingLink.graphql +5 -0
  26. package/components/TrackingLink/TrackingLink.tsx +32 -0
  27. package/components/WaitForCustomer/WaitForCustomer.tsx +27 -21
  28. package/components/index.ts +14 -0
  29. package/graphql/AccountDashboard.graphql +16 -0
  30. package/graphql/AccountDashboardAddresses.graphql +5 -0
  31. package/graphql/AccountDashboardCustomer.graphql +3 -0
  32. package/graphql/AccountDashboardOrders.graphql +5 -0
  33. package/graphql/OrderDetailPage.graphql +10 -0
  34. package/hooks/OrderCardItemImage.graphql +7 -0
  35. package/hooks/OrderCardItemImages.graphql +7 -0
  36. package/hooks/UseOrderCardItemImages.graphql +7 -0
  37. package/hooks/index.ts +4 -0
  38. package/hooks/useOrderCardItemImages.ts +22 -0
  39. package/index.ts +4 -0
  40. package/package.json +13 -13
  41. package/test/authentication.playwright.ts +65 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 5.2.0-canary.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1768](https://github.com/graphcommerce-org/graphcommerce/pull/1768) [`23e9a4728`](https://github.com/graphcommerce-org/graphcommerce/commit/23e9a472899dfc0b56b989f5d0e8ffb802c8cc5f) - Move magento-customer-account & magento-customer-order into magento-customer package (magento-customer-account & magento-customer-order are now deprecated) ([@bramvanderholst](https://github.com/bramvanderholst))
8
+
9
+ - [#1768](https://github.com/graphcommerce-org/graphcommerce/pull/1768) [`7e8dcf447`](https://github.com/graphcommerce-org/graphcommerce/commit/7e8dcf44777aca527c07aaee397d272dd2f6ae44) - Update account address list after deleting an address ([@bramvanderholst](https://github.com/bramvanderholst))
10
+
11
+ - [#1768](https://github.com/graphcommerce-org/graphcommerce/pull/1768) [`9f0e9ab2d`](https://github.com/graphcommerce-org/graphcommerce/commit/9f0e9ab2dec3f9261ae00e9fd44d06a65ddb1d0d) - Redirect to address overview page after creating an address instead of redirecting to the edit page for the address that was just created ([@bramvanderholst](https://github.com/bramvanderholst))
12
+
13
+ ## 5.2.0-canary.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#1769](https://github.com/graphcommerce-org/graphcommerce/pull/1769) [`2693a616a`](https://github.com/graphcommerce-org/graphcommerce/commit/2693a616af2f9793012a5fb2eeacc084e695b83e) - WaitForCustomer now accepts overridable components for the fallback and unauthenticated state. ([@mikekeehnen](https://github.com/mikekeehnen))
18
+
3
19
  ## 5.1.1
4
20
 
5
21
  ## 5.1.1-canary.1
@@ -0,0 +1,31 @@
1
+ import { SectionContainer } from '@graphcommerce/next-ui'
2
+ import React from 'react'
3
+ import useOrderCardItemImages from '../../hooks/useOrderCardItemImages'
4
+ import { AccountOrdersFragment } from '../AccountOrders/AccountOrders.gql'
5
+ import { NoOrdersFound } from '../NoOrdersFound/NoOrdersFound'
6
+ import { OrderCard } from '../OrderCard/OrderCard'
7
+
8
+ export type AccountLatestOrderProps = AccountOrdersFragment & {
9
+ loading: boolean
10
+ }
11
+
12
+ export function AccountLatestOrder(props: AccountLatestOrderProps) {
13
+ const { orders, loading } = props
14
+ const latestOrderCard = orders?.items?.[(orders?.items?.length ?? 1) - 1]
15
+ const images = useOrderCardItemImages(orders)
16
+
17
+ // TODO: when Magento fixes their API sorting
18
+ // const latestOrderCard = orders?.items?.[0]
19
+
20
+ return (
21
+ <SectionContainer labelLeft='Latest order'>
22
+ {!loading && (
23
+ <>
24
+ {!latestOrderCard && <NoOrdersFound />}
25
+ {latestOrderCard && <OrderCard {...latestOrderCard} images={images} />}
26
+ </>
27
+ )}
28
+ {loading && <OrderCard loading />}
29
+ </SectionContainer>
30
+ )
31
+ }
@@ -0,0 +1,21 @@
1
+ import { extendableComponent } from '@graphcommerce/next-ui'
2
+ import { List, SxProps, Theme } from '@mui/material'
3
+ import React from 'react'
4
+
5
+ export type AccountMenuProps = { children: React.ReactNode; sx?: SxProps<Theme> }
6
+
7
+ const { classes } = extendableComponent('AccountMenu', ['root'] as const)
8
+
9
+ export function AccountMenu(props: AccountMenuProps) {
10
+ const { children, sx = [] } = props
11
+
12
+ return (
13
+ <List
14
+ classes={classes}
15
+ disablePadding
16
+ sx={[(theme) => ({ marginBottom: theme.spacings.lg }), ...(Array.isArray(sx) ? sx : [sx])]}
17
+ >
18
+ {children}
19
+ </List>
20
+ )
21
+ }
@@ -0,0 +1,105 @@
1
+ import { ImageProps } from '@graphcommerce/image'
2
+ import {
3
+ responsiveVal,
4
+ iconChevronRight,
5
+ IconSvg,
6
+ Button,
7
+ ButtonProps,
8
+ extendableComponent,
9
+ } from '@graphcommerce/next-ui'
10
+ import { ListItem, ListItemIcon, ListItemText, SxProps, Theme } from '@mui/material'
11
+ import PageLink from 'next/link'
12
+ import React from 'react'
13
+
14
+ export type AccountMenuItemProps = {
15
+ iconSrc: ImageProps['src']
16
+ title: React.ReactNode
17
+ subtitle?: React.ReactNode
18
+ endIcon?: React.ReactNode
19
+ sx?: SxProps<Theme>
20
+ } & Omit<ButtonProps, 'endIcon' | 'startIcon' | 'title'> &
21
+ OwnerState
22
+
23
+ type OwnerState = { noBorderBottom?: boolean }
24
+ const name = 'AccountMenuItem'
25
+ const parts = ['root', 'icon'] as const
26
+ const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
27
+
28
+ export function AccountMenuItem(props: AccountMenuItemProps) {
29
+ const {
30
+ title,
31
+ subtitle,
32
+ iconSrc,
33
+ endIcon,
34
+ href,
35
+ disabled,
36
+ noBorderBottom = false,
37
+ sx = [],
38
+ ...buttonProps
39
+ } = props
40
+ const classes = withState({ noBorderBottom })
41
+
42
+ const button = (
43
+ <Button
44
+ disabled={disabled}
45
+ className={classes.root}
46
+ sx={[
47
+ (theme) => ({
48
+ width: '100%',
49
+ height: responsiveVal(88, 104),
50
+ padding: 0,
51
+ borderRadius: 0,
52
+ background: theme.palette.background.paper,
53
+ '&:hover': {
54
+ background: theme.palette.background.default,
55
+ },
56
+ '&:disabled': {
57
+ background: theme.palette.background.default,
58
+ },
59
+ '&:focus': {
60
+ // fix: disableElevation does not work when button is focused
61
+ boxShadow: 'none',
62
+ },
63
+
64
+ '&:not(.noBorderBottom)': {
65
+ borderBottom: `1px solid ${theme.palette.divider}`,
66
+ },
67
+ }),
68
+ ...(Array.isArray(sx) ? sx : [sx]),
69
+ ]}
70
+ {...buttonProps}
71
+ >
72
+ <ListItem disableGutters>
73
+ <ListItemIcon
74
+ className={classes.icon}
75
+ sx={(theme) => ({
76
+ paddingRight: theme.spacings.xs,
77
+ })}
78
+ >
79
+ <IconSvg src={iconSrc} size='large' />
80
+ </ListItemIcon>
81
+ <ListItemText
82
+ primaryTypographyProps={{ sx: { typography: 'subtitle1' } }}
83
+ secondaryTypographyProps={{
84
+ sx: {
85
+ whiteSpace: 'nowrap',
86
+ overflow: 'hidden',
87
+ textOverflow: 'elipsis',
88
+ },
89
+ }}
90
+ primary={title}
91
+ secondary={subtitle}
92
+ />
93
+ {endIcon ?? <IconSvg src={iconChevronRight} />}
94
+ </ListItem>
95
+ </Button>
96
+ )
97
+
98
+ return href ? (
99
+ <PageLink href={href} passHref>
100
+ {button}
101
+ </PageLink>
102
+ ) : (
103
+ button
104
+ )
105
+ }
@@ -0,0 +1,13 @@
1
+ fragment AccountOrders on Customer {
2
+ orders(filter: {}, pageSize: $pageSize, currentPage: $currentPage) {
3
+ items {
4
+ ...OrderCard
5
+ }
6
+ page_info {
7
+ current_page
8
+ page_size
9
+ total_pages
10
+ }
11
+ total_count
12
+ }
13
+ }
@@ -0,0 +1,88 @@
1
+ import { Pagination, SectionContainer, extendableComponent } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/react'
3
+ import { Box, Link, SxProps, Theme } from '@mui/material'
4
+ import PageLink from 'next/link'
5
+ import React from 'react'
6
+ import useOrderCardItemImages from '../../hooks/useOrderCardItemImages'
7
+ import { NoOrdersFound } from '../NoOrdersFound/NoOrdersFound'
8
+ import { OrderCard } from '../OrderCard/OrderCard'
9
+ import { AccountOrdersFragment } from './AccountOrders.gql'
10
+
11
+ export type AccountOrdersProps = AccountOrdersFragment & { sx?: SxProps<Theme> }
12
+
13
+ const parts = ['root', 'older'] as const
14
+ const { classes } = extendableComponent('AccountOrders', parts)
15
+
16
+ export function AccountOrders(props: AccountOrdersProps) {
17
+ const { orders, sx = [] } = props
18
+ const amountLatestOrders = 2
19
+ const images = useOrderCardItemImages(orders)
20
+
21
+ const pageInfo = orders?.page_info
22
+ const isFirstPage = pageInfo?.current_page === 1
23
+
24
+ // whenever it's possible, pick last {amountLatestOrders} items, then reverse the resulting array,
25
+ // because we want to render the latest order first,
26
+ // but the API returns the orders in ASC order...
27
+ const latestOrders = orders?.items
28
+ .slice(Math.max((orders?.items?.length ?? 0) - 2, 0), orders?.items?.length)
29
+ .reverse()
30
+
31
+ const olderOrders = isFirstPage
32
+ ? orders?.items.slice(0, Math.max((orders?.items?.length ?? 0) - 2, 0)).reverse()
33
+ : orders?.items
34
+
35
+ return (
36
+ <Box
37
+ className={classes.root}
38
+ sx={[
39
+ (theme) => ({
40
+ typography: 'body2',
41
+ marginBottom: theme.spacings.md,
42
+ }),
43
+ ...(Array.isArray(sx) ? sx : [sx]),
44
+ ]}
45
+ >
46
+ {isFirstPage && (
47
+ <SectionContainer labelLeft={<Trans id='Latest orders' />}>
48
+ {latestOrders?.map(
49
+ (order) => order && <OrderCard key={order.number} {...order} images={images} />,
50
+ )}
51
+ {orders?.items && !orders?.items?.length && <NoOrdersFound />}
52
+ </SectionContainer>
53
+ )}
54
+
55
+ {orders?.items &&
56
+ ((isFirstPage && orders?.items?.length >= amountLatestOrders + 1) || !isFirstPage) && (
57
+ <SectionContainer
58
+ labelLeft={<Trans id='Older' />}
59
+ className={classes.older}
60
+ sx={(theme) => ({
61
+ [theme.breakpoints.up('md')]: {
62
+ marginTop: theme.spacings.lg,
63
+ marginBottom: theme.spacings.lg,
64
+ },
65
+ marginTop: theme.spacings.md,
66
+ marginBottom: theme.spacings.md,
67
+ })}
68
+ >
69
+ {olderOrders?.map(
70
+ (order) => order && <OrderCard key={order.number} {...order} images={images} />,
71
+ )}
72
+ </SectionContainer>
73
+ )}
74
+
75
+ <Pagination
76
+ count={pageInfo?.total_pages ?? 1}
77
+ page={pageInfo?.current_page ?? 1}
78
+ renderLink={(p: number, icon: React.ReactNode) => (
79
+ <PageLink href={p === 1 ? '/account/orders' : `/account/orders?page=${p}`} passHref>
80
+ <Link color='primary' underline='hover'>
81
+ {icon}
82
+ </Link>
83
+ </PageLink>
84
+ )}
85
+ />
86
+ </Box>
87
+ )
88
+ }
@@ -0,0 +1,167 @@
1
+ import {
2
+ ApolloCustomerErrorAlert,
3
+ SignInForm,
4
+ SignUpForm,
5
+ useFormIsEmailAvailable,
6
+ CustomerDocument,
7
+ } from '@graphcommerce/magento-customer'
8
+ import { useCustomerQuery } from '@graphcommerce/magento-customer/hooks'
9
+ import {
10
+ Button,
11
+ FormDiv,
12
+ FormActions,
13
+ FormRow,
14
+ LayoutTitle,
15
+ extendableComponent,
16
+ } from '@graphcommerce/next-ui'
17
+ import { emailPattern } from '@graphcommerce/react-hook-form'
18
+ import { Trans } from '@lingui/react'
19
+ import { Box, CircularProgress, Link, SxProps, TextField, Theme, Typography } from '@mui/material'
20
+ import PageLink from 'next/link'
21
+ import router from 'next/router'
22
+
23
+ export type AccountSignInUpFormProps = { sx?: SxProps<Theme> }
24
+
25
+ const parts = ['root', 'titleContainer'] as const
26
+ const { classes } = extendableComponent('AccountSignInUpForm', parts)
27
+
28
+ const titleContainerSx: SxProps<Theme> = (theme) => ({
29
+ typography: 'body1',
30
+ marginBottom: theme.spacings.xs,
31
+ })
32
+
33
+ export function AccountSignInUpForm(props: AccountSignInUpFormProps) {
34
+ const { sx = [] } = props
35
+ const customerQuery = useCustomerQuery(CustomerDocument)
36
+
37
+ const { email, firstname = '' } = customerQuery.data?.customer ?? {}
38
+ const { mode, form, autoSubmitting, submit } = useFormIsEmailAvailable({ email })
39
+ const { formState, muiRegister, required, watch, error } = form
40
+ const disableFields = formState.isSubmitting && !autoSubmitting
41
+
42
+ return (
43
+ <FormDiv sx={sx} className={classes.root}>
44
+ {mode === 'email' && (
45
+ <Box className={classes.titleContainer} sx={titleContainerSx}>
46
+ <LayoutTitle variant='h2' gutterBottom={false}>
47
+ <Trans id='Sign in or create an account!' />
48
+ </LayoutTitle>
49
+ <Typography variant='h6' align='center'>
50
+ <Trans id='Fill in your e-mail to login or create an account' />
51
+ </Typography>
52
+ </Box>
53
+ )}
54
+
55
+ {mode === 'signin' && (
56
+ <Box className={classes.titleContainer} sx={titleContainerSx}>
57
+ <LayoutTitle variant='h2' gutterBottom={false}>
58
+ <Trans id='Welcome back!' />
59
+ </LayoutTitle>
60
+ <Typography variant='h6' align='center'>
61
+ <Trans id='Fill in your password' />
62
+ </Typography>
63
+ </Box>
64
+ )}
65
+
66
+ {mode === 'signup' && (
67
+ <Box className={classes.titleContainer} sx={titleContainerSx}>
68
+ <LayoutTitle variant='h2' gutterBottom={false}>
69
+ <Trans id='Create account!' />
70
+ </LayoutTitle>
71
+ <Typography variant='h6' align='center'>
72
+ <Trans id='Create a password and tell us your name' />
73
+ </Typography>
74
+ </Box>
75
+ )}
76
+
77
+ {mode === 'signedin' && (
78
+ <Box className={classes.titleContainer} sx={titleContainerSx}>
79
+ <LayoutTitle variant='h2' gutterBottom={false}>
80
+ <Trans id='Hi {firstname}! You’re now logged in!' values={{ firstname }} />
81
+ </LayoutTitle>
82
+ <Typography variant='h6' align='center'>
83
+ <PageLink href='/account' passHref>
84
+ <Link underline='hover' color='secondary'>
85
+ <Trans id='View your account' />
86
+ </Link>
87
+ </PageLink>
88
+ </Typography>
89
+
90
+ <FormActions>
91
+ <Button onClick={() => router.back()} variant='pill' color='secondary' size='large'>
92
+ <Trans id='Continue shopping' />
93
+ </Button>
94
+ </FormActions>
95
+ </Box>
96
+ )}
97
+
98
+ {mode === 'session-expired' && (
99
+ <Box className={classes.titleContainer} sx={titleContainerSx}>
100
+ <LayoutTitle variant='h2' gutterBottom={false}>
101
+ <Trans id='Your session is expired' />
102
+ </LayoutTitle>
103
+ <Typography variant='h6' align='center'>
104
+ <Trans id='Log in to continue shopping' />
105
+ </Typography>
106
+ </Box>
107
+ )}
108
+
109
+ {mode !== 'signedin' && (
110
+ <form noValidate onSubmit={submit}>
111
+ <Box>
112
+ <FormRow>
113
+ <TextField
114
+ variant='outlined'
115
+ type='text'
116
+ autoComplete='email'
117
+ error={formState.isSubmitted && !!formState.errors.email}
118
+ helperText={formState.isSubmitted && formState.errors.email?.message}
119
+ label={<Trans id='Email' />}
120
+ required={required.email}
121
+ disabled={disableFields}
122
+ {...muiRegister('email', {
123
+ required: required.email,
124
+ pattern: { value: emailPattern, message: '' },
125
+ })}
126
+ InputProps={{
127
+ endAdornment: formState.isSubmitting && <CircularProgress />,
128
+ readOnly: !!customerQuery.data?.customer?.email,
129
+ }}
130
+ />
131
+ </FormRow>
132
+ </Box>
133
+
134
+ <ApolloCustomerErrorAlert error={error} />
135
+
136
+ {(mode === 'email' || mode === 'session-expired') && (
137
+ <Box>
138
+ <FormActions>
139
+ <Button
140
+ type='submit'
141
+ loading={formState.isSubmitting}
142
+ variant='pill'
143
+ color='primary'
144
+ size='large'
145
+ >
146
+ <Trans id='Continue' />
147
+ </Button>
148
+ </FormActions>
149
+ </Box>
150
+ )}
151
+ </form>
152
+ )}
153
+
154
+ {mode === 'signin' && (
155
+ <Box>
156
+ <SignInForm email={watch('email')} />
157
+ </Box>
158
+ )}
159
+
160
+ {mode === 'signup' && (
161
+ <Box>
162
+ <SignUpForm email={watch('email')} />
163
+ </Box>
164
+ )}
165
+ </FormDiv>
166
+ )
167
+ }
@@ -44,7 +44,7 @@ export function CreateCustomerAddressForm() {
44
44
  },
45
45
  onComplete: (e) => {
46
46
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
47
- router.push(`/account/addresses/edit?addressId=${e.data?.createCustomerAddress?.id}`)
47
+ router.push(`/account/addresses`)
48
48
  },
49
49
  },
50
50
  { errorPolicy: 'all' },
@@ -2,6 +2,8 @@ import { ApolloErrorSnackbar } from '@graphcommerce/ecommerce-ui'
2
2
  import { useFormGqlMutation } from '@graphcommerce/react-hook-form'
3
3
  import { Trans } from '@lingui/react'
4
4
  import { Button } from '@mui/material'
5
+ import { AccountDashboardAddressesDocument } from '../../graphql/AccountDashboardAddresses.gql'
6
+ import { useCustomerQuery } from '../../hooks/useCustomerQuery'
5
7
  import { DeleteCustomerAddressFormDocument } from './DeleteCustomerAddressForm.gql'
6
8
 
7
9
  export type DeleteCustomerAddressFormProps = {
@@ -10,10 +12,18 @@ export type DeleteCustomerAddressFormProps = {
10
12
 
11
13
  export function DeleteCustomerAddressForm(props: DeleteCustomerAddressFormProps) {
12
14
  const { addressId } = props
15
+ const { refetch } = useCustomerQuery(AccountDashboardAddressesDocument, {
16
+ fetchPolicy: 'cache-and-network',
17
+ })
13
18
  const { handleSubmit, error } = useFormGqlMutation(
14
19
  DeleteCustomerAddressFormDocument,
15
20
  { defaultValues: { id: addressId } },
16
- { errorPolicy: 'all' },
21
+ {
22
+ errorPolicy: 'all',
23
+ onCompleted: async () => {
24
+ await refetch()
25
+ },
26
+ },
17
27
  )
18
28
  const submitHandler = handleSubmit(() => {})
19
29
 
@@ -0,0 +1,22 @@
1
+ import { IconHeader, iconBox, extendableComponent } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/react'
3
+ import { Box, SxProps, Theme } from '@mui/material'
4
+
5
+ const parts = ['root'] as const
6
+ const { classes } = extendableComponent('NoOrdersFound', parts)
7
+
8
+ type NoOrdersFoundProps = { sx?: SxProps<Theme> }
9
+
10
+ export function NoOrdersFound(props: NoOrdersFoundProps) {
11
+ const { sx = [] } = props
12
+ return (
13
+ <Box
14
+ className={classes.root}
15
+ sx={[(theme) => ({ marginTop: theme.spacings.sm }), ...(Array.isArray(sx) ? sx : [sx])]}
16
+ >
17
+ <IconHeader src={iconBox} size='small'>
18
+ <Trans id='No orders found' />
19
+ </IconHeader>
20
+ </Box>
21
+ )
22
+ }
@@ -0,0 +1,18 @@
1
+ fragment OrderCard on CustomerOrder {
2
+ number
3
+ shipments {
4
+ tracking {
5
+ ...TrackingLink
6
+ }
7
+ }
8
+ total {
9
+ grand_total {
10
+ ...Money
11
+ }
12
+ }
13
+ items {
14
+ ...OrderCardItem
15
+ }
16
+ ...OrderStateLabel
17
+ order_date
18
+ }