├── .gitignore
├── .travis.yml
├── LICENSE
├── Nova
├── Cart.php
├── CartItem.php
├── Coupon.php
├── Customer.php
├── Order.php
├── OrderItem.php
├── Product.php
└── Resource.php
├── README.md
├── composer.json
├── composer.lock
├── config
└── checkout.php
├── database
├── .gitignore
├── factories
│ ├── CartFactory.php
│ ├── CartItemFactory.php
│ ├── CouponFactory.php
│ ├── CustomerFactory.php
│ ├── OrderFactory.php
│ ├── OrderItemFactory.php
│ └── ProductFactory.php
└── migrations
│ ├── 2020_02_17_1_create_products_table.php
│ ├── 2020_02_17_2_create_customers_table.php
│ ├── 2020_02_17_3_create_coupons_table.php
│ ├── 2020_02_17_4_create_carts_table.php
│ ├── 2020_02_17_5_create_cart_items_table.php
│ ├── 2020_02_17_6_create_orders_table.php
│ ├── 2020_02_17_7_create_order_items_table.php
│ ├── 2020_02_17_8_create_order_purchases_table.php
│ ├── 2020_03_24_9_add_options_to_carts_and_orders_table.php
│ └── 2020_06_03_10_add_paypal_columns_to_order_purchases_table.php
├── phpunit.xml.dist
├── routes
└── api_guest.php
├── src
├── CheckoutFields.php
├── CheckoutServiceProvider.php
├── ConfigurableModel.php
├── Contracts
│ ├── Cart.php
│ ├── CartItem.php
│ ├── Coupon.php
│ ├── Customer.php
│ ├── Order.php
│ ├── OrderItem.php
│ ├── PaymentHandler.php
│ ├── Product.php
│ └── State.php
├── Events
│ ├── NewOrder.php
│ └── NewOrderPurchase.php
├── Facades
│ ├── AddressSearch.php
│ ├── Cart.php
│ ├── CartItem.php
│ ├── Coupon.php
│ ├── Customer.php
│ ├── Order.php
│ ├── OrderItem.php
│ ├── Product.php
│ └── Shipping.php
├── GuestCustomer.php
├── Helpers
│ ├── Address
│ │ ├── Address.php
│ │ ├── AddressSearch.php
│ │ ├── FakeGeoNames.php
│ │ └── GeoNames.php
│ ├── Globals.php
│ ├── HashTagExtractor.php
│ ├── Price.php
│ ├── Responder.php
│ ├── Search.php
│ ├── Token.php
│ └── ToolKit.php
├── Http
│ ├── Controllers
│ │ ├── CartController.php
│ │ ├── CartCouponController.php
│ │ ├── CartEmailController.php
│ │ ├── CartItemController.php
│ │ ├── CartOptionsController.php
│ │ ├── CartShippingController.php
│ │ ├── CartZipCodeController.php
│ │ ├── CheckoutSettingsController.php
│ │ ├── Controller.php
│ │ └── OrderController.php
│ ├── Requests
│ │ ├── CartCouponRequest.php
│ │ ├── CartEmailRequest.php
│ │ ├── CartItemRequest.php
│ │ ├── CartOptionsRequest.php
│ │ ├── CartRequest.php
│ │ ├── CartZipCodeRequest.php
│ │ ├── JsonFormRequest.php
│ │ ├── OrderItemRequest.php
│ │ └── OrderRequest.php
│ └── Resources
│ │ ├── CartItemResource.php
│ │ ├── CartResource.php
│ │ ├── OrderItemResource.php
│ │ ├── OrderPurchaseResource.php
│ │ ├── OrderResource.php
│ │ └── ProductResource.php
├── Models
│ ├── Cart.php
│ ├── CartItem.php
│ ├── Coupon.php
│ ├── Customer.php
│ ├── Order.php
│ ├── OrderItem.php
│ ├── OrderPurchase.php
│ └── Product.php
├── PaymentException.php
├── PaymentHandlerFactory.php
├── PaypalPaymentHandler.php
├── State.php
├── StripeMockHandler.php
└── StripePaymentHandler.php
└── tests
├── .gitkeep
├── Controllers
├── CartControllerTest.php
├── CartCouponControllerTest.php
├── CartItemControllerTest.php
├── CartZipCodeControllerTest.php
├── CheckoutSettingsControllerTest.php
└── OrderControllerTest.php
├── TestCase.php
└── Unit
└── AddressSearchTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/hot
3 | /public/storage
4 | /public/js/app.js
5 | /public/css/app.css
6 | /storage/*.key
7 | /vendor
8 | /.idea
9 | /.vagrant
10 | Homestead.json
11 | Homestead.yaml
12 | npm-debug.log
13 | yarn-error.log
14 | .env
15 | .phpunit.result.cache
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: bionic
2 | language: php
3 |
4 | cache:
5 | directories:
6 | - $HOME/.composer/cache
7 |
8 | matrix:
9 | fast_finish: true
10 | include:
11 | - php: 7.3
12 | env: SETUP=stable
13 |
14 | before_install:
15 | - travis_retry composer self-update
16 |
17 | install:
18 | - if [[ $SETUP = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest; fi
19 |
20 | script:
21 | - vendor/bin/phpunit
22 |
23 | notifications:
24 | email: false
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-present, Chris Fritz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Nova/Cart.php:
--------------------------------------------------------------------------------
1 | sortable(),
61 |
62 | BelongsTo::make('Customer', 'customer', \App\Nova\Customer::class),
63 |
64 | BelongsTo::make('Coupon', 'coupon', \App\Nova\Coupon::class),
65 |
66 | HasMany::make('Cart Items', 'cartItems', \App\Nova\CartItem::class),
67 |
68 | Text::make('Shipping First Name', 'shipping_first_name'),
69 | Text::make('Shipping Last Name', 'shipping_last_name'),
70 | Text::make('Shipping Address Line1', 'shipping_address_line1'),
71 | Text::make('Shipping Address Line2', 'shipping_address_line2'),
72 | Text::make('Shipping Address City', 'shipping_address_city'),
73 | Text::make('Shipping Address Region', 'shipping_address_region'),
74 | Text::make('Shipping Address Zipcode', 'shipping_address_zipcode'),
75 | Text::make('Shipping Address Phone', 'shipping_address_phone'),
76 | Text::make('Billing First Name', 'billing_first_name'),
77 | Text::make('Billing Last Name', 'billing_last_name'),
78 | Text::make('Billing Address Line1', 'billing_address_line1'),
79 | Text::make('Billing Address Line2', 'billing_address_line2'),
80 | Text::make('Billing Address City', 'billing_address_city'),
81 | Text::make('Billing Address Region', 'billing_address_region'),
82 | Text::make('Billing Address Zipcode', 'billing_address_zipcode'),
83 | Text::make('Billing Address Phone', 'billing_address_phone'),
84 |
85 | Text::make('Token', 'token'),
86 | DateTime::make('Created At'),
87 | ];
88 | }
89 |
90 | /**
91 | * Get the cards available for the request.
92 | *
93 | * @param \Illuminate\Http\Request $request
94 | * @return array
95 | */
96 | public function cards(Request $request)
97 | {
98 | return [];
99 | }
100 |
101 | /**
102 | * Get the filters available for the resource.
103 | *
104 | * @param \Illuminate\Http\Request $request
105 | * @return array
106 | */
107 | public function filters(Request $request)
108 | {
109 | return [];
110 | }
111 |
112 | /**
113 | * Get the lenses available for the resource.
114 | *
115 | * @param \Illuminate\Http\Request $request
116 | * @return array
117 | */
118 | public function lenses(Request $request)
119 | {
120 | return [];
121 | }
122 |
123 | /**
124 | * Get the actions available for the resource.
125 | *
126 | * @param \Illuminate\Http\Request $request
127 | * @return array
128 | */
129 | public function actions(Request $request)
130 | {
131 | return [];
132 | }
133 |
134 | /***************************************************************************************
135 | ** HELPERS
136 | ***************************************************************************************/
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/Nova/CartItem.php:
--------------------------------------------------------------------------------
1 | sortable(),
64 |
65 | BelongsTo::make('Cart', 'cart', \App\Nova\Cart::class),
66 |
67 | BelongsTo::make('Product', 'product', \App\Nova\Product::class),
68 |
69 | DateTime::make('Created At'),
70 | ];
71 | }
72 |
73 | /**
74 | * Get the cards available for the request.
75 | *
76 | * @param \Illuminate\Http\Request $request
77 | * @return array
78 | */
79 | public function cards(Request $request)
80 | {
81 | return [];
82 | }
83 |
84 | /**
85 | * Get the filters available for the resource.
86 | *
87 | * @param \Illuminate\Http\Request $request
88 | * @return array
89 | */
90 | public function filters(Request $request)
91 | {
92 | return [];
93 | }
94 |
95 | /**
96 | * Get the lenses available for the resource.
97 | *
98 | * @param \Illuminate\Http\Request $request
99 | * @return array
100 | */
101 | public function lenses(Request $request)
102 | {
103 | return [];
104 | }
105 |
106 | /**
107 | * Get the actions available for the resource.
108 | *
109 | * @param \Illuminate\Http\Request $request
110 | * @return array
111 | */
112 | public function actions(Request $request)
113 | {
114 | return [];
115 | }
116 |
117 | /***************************************************************************************
118 | ** HELPERS
119 | ***************************************************************************************/
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Nova/Coupon.php:
--------------------------------------------------------------------------------
1 | sortable(),
38 |
39 | Text::make('Name', 'name')
40 | ->sortable()
41 | ->rules('required', 'max:255'),
42 |
43 | Text::make('Code', 'code')
44 | ->rules('required', 'max:255'),
45 |
46 | Number::make('Discount', 'discount')
47 | ->sortable()
48 | ->rules('required', 'numeric'),
49 |
50 | Boolean::make('Percentage', 'percentage')
51 | ->rules('required', 'boolean'),
52 |
53 | HasMany::make('Orders', 'orders', \App\Nova\Order::class)
54 | ];
55 | }
56 |
57 | /**
58 | * Get the cards available for the request.
59 | *
60 | * @param \Illuminate\Http\Request $request
61 | * @return array
62 | */
63 | public function cards(Request $request)
64 | {
65 | return [];
66 | }
67 |
68 | /**
69 | * Get the filters available for the resource.
70 | *
71 | * @param \Illuminate\Http\Request $request
72 | * @return array
73 | */
74 | public function filters(Request $request)
75 | {
76 | return [];
77 | }
78 |
79 | /**
80 | * Get the lenses available for the resource.
81 | *
82 | * @param \Illuminate\Http\Request $request
83 | * @return array
84 | */
85 | public function lenses(Request $request)
86 | {
87 | return [];
88 | }
89 |
90 | /**
91 | * Get the actions available for the resource.
92 | *
93 | * @param \Illuminate\Http\Request $request
94 | * @return array
95 | */
96 | public function actions(Request $request)
97 | {
98 | return [];
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Nova/Customer.php:
--------------------------------------------------------------------------------
1 | sortable(),
63 |
64 | Text::make('First Name', 'first_name')
65 | ->sortable()
66 | ->rules('required', 'max:255'),
67 |
68 | Text::make('Last Name', 'last_name')
69 | ->sortable()
70 | ->rules('required', 'max:255'),
71 |
72 | Text::make('Email', 'email')
73 | ->sortable()
74 | ->rules('required', 'email', 'max:254')
75 | ->creationRules('unique:users,email')
76 | ->updateRules('unique:users,email,{{resourceId}}'),
77 |
78 | Password::make('Password', 'password')
79 | ->onlyOnForms()
80 | ->creationRules('required', 'string', 'min:8')
81 | ->updateRules('nullable', 'string', 'min:8'),
82 |
83 | Text::make('Phone', 'phone')
84 | ->rules('required', 'max:255'),
85 |
86 | DateTime::make('Created At'),
87 | ];
88 | }
89 |
90 | /**
91 | * Get the cards available for the request.
92 | *
93 | * @param \Illuminate\Http\Request $request
94 | * @return array
95 | */
96 | public function cards(Request $request)
97 | {
98 | return [];
99 | }
100 |
101 | /**
102 | * Get the filters available for the resource.
103 | *
104 | * @param \Illuminate\Http\Request $request
105 | * @return array
106 | */
107 | public function filters(Request $request)
108 | {
109 | return [];
110 | }
111 |
112 | /**
113 | * Get the lenses available for the resource.
114 | *
115 | * @param \Illuminate\Http\Request $request
116 | * @return array
117 | */
118 | public function lenses(Request $request)
119 | {
120 | return [];
121 | }
122 |
123 | /**
124 | * Get the actions available for the resource.
125 | *
126 | * @param \Illuminate\Http\Request $request
127 | * @return array
128 | */
129 | public function actions(Request $request)
130 | {
131 | return [];
132 | }
133 |
134 | public function title()
135 | {
136 | return $this->first_name . ' ' . $this->last_name;
137 | }
138 |
139 | /***************************************************************************************
140 | ** HELPERS
141 | ***************************************************************************************/
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/Nova/Order.php:
--------------------------------------------------------------------------------
1 | sortable(),
66 |
67 | BelongsTo::make('Cart', 'cart', \App\Nova\Cart::class),
68 | BelongsTo::make('Customer', 'customer', \App\Nova\Order::class),
69 | HasMany::make('Order Items', 'orderItems', \App\Nova\OrderItem::class),
70 | HasOne::make('Order purchase', 'orderPurchase', \App\Nova\OrderPurchase::class),
71 |
72 | Text::make('Customer Email', 'customer_email'),
73 | Text::make('Currency', 'currency'),
74 | Text::make('Shipping First Name', 'shipping_first_name'),
75 | Text::make('Shipping Last Name', 'shipping_last_name'),
76 | Text::make('Shipping Address Line1', 'shipping_address_line1'),
77 | Text::make('Shipping Address Line2', 'shipping_address_line2'),
78 | Text::make('Shipping Address City', 'shipping_address_city'),
79 | Text::make('Shipping Address Region', 'shipping_address_region'),
80 | Text::make('Shipping Address Zipcode', 'shipping_address_zipcode'),
81 | Text::make('Shipping Address Phone', 'shipping_address_phone'),
82 | Text::make('Billing Address Line1', 'billing_address_line1'),
83 | Text::make('Billing Address Line2', 'billing_address_line2'),
84 | Text::make('Billing Address City', 'billing_address_city'),
85 | Text::make('Billing Address Region', 'billing_address_region'),
86 | Text::make('Billing Address Zipcode', 'billing_address_zipcode'),
87 | Text::make('Billing Address Phone', 'billing_address_phone'),
88 | Text::make('Status', 'status'),
89 |
90 | Textarea::make('Customer Notes', 'customer_notes'),
91 | Textarea::make('Admin Notes', 'admin_notes'),
92 |
93 | DateTime::make('Created At'),
94 | ];
95 | }
96 |
97 | /**
98 | * Get the cards available for the request.
99 | *
100 | * @param \Illuminate\Http\Request $request
101 | * @return array
102 | */
103 | public function cards(Request $request)
104 | {
105 | return [];
106 | }
107 |
108 | /**
109 | * Get the filters available for the resource.
110 | *
111 | * @param \Illuminate\Http\Request $request
112 | * @return array
113 | */
114 | public function filters(Request $request)
115 | {
116 | return [];
117 | }
118 |
119 | /**
120 | * Get the lenses available for the resource.
121 | *
122 | * @param \Illuminate\Http\Request $request
123 | * @return array
124 | */
125 | public function lenses(Request $request)
126 | {
127 | return [];
128 | }
129 |
130 | /**
131 | * Get the actions available for the resource.
132 | *
133 | * @param \Illuminate\Http\Request $request
134 | * @return array
135 | */
136 | public function actions(Request $request)
137 | {
138 | return [];
139 | }
140 |
141 | /***************************************************************************************
142 | ** HELPERS
143 | ***************************************************************************************/
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/Nova/OrderItem.php:
--------------------------------------------------------------------------------
1 | sortable(),
64 |
65 | BelongsTo::make('Product', 'product', \App\Nova\OrderItem::class),
66 |
67 | BelongsTo::make('Cart Item', 'cartItem', \App\Nova\OrderItem::class),
68 |
69 | Text::make('Name', 'name'),
70 |
71 | Text::make('Customer Note', 'customer_note'),
72 |
73 | DateTime::make('Created At')->format('MM/DD h:mm a'),
74 | ];
75 | }
76 |
77 | /**
78 | * Get the cards available for the request.
79 | *
80 | * @param \Illuminate\Http\Request $request
81 | * @return array
82 | */
83 | public function cards(Request $request)
84 | {
85 | return [];
86 | }
87 |
88 | /**
89 | * Get the filters available for the resource.
90 | *
91 | * @param \Illuminate\Http\Request $request
92 | * @return array
93 | */
94 | public function filters(Request $request)
95 | {
96 | return [];
97 | }
98 |
99 | /**
100 | * Get the lenses available for the resource.
101 | *
102 | * @param \Illuminate\Http\Request $request
103 | * @return array
104 | */
105 | public function lenses(Request $request)
106 | {
107 | return [];
108 | }
109 |
110 | /**
111 | * Get the actions available for the resource.
112 | *
113 | * @param \Illuminate\Http\Request $request
114 | * @return array
115 | */
116 | public function actions(Request $request)
117 | {
118 | return [];
119 | }
120 |
121 | /***************************************************************************************
122 | ** HELPERS
123 | ***************************************************************************************/
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/Nova/Product.php:
--------------------------------------------------------------------------------
1 | sortable(),
66 |
67 | Text::make('Name', 'name'),
68 |
69 | Text::make('Price', 'price')
70 | ->sortable()
71 | ->rules('required', 'max:255'),
72 |
73 | DateTime::make('Created At'),
74 | ];
75 | }
76 |
77 | /**
78 | * Get the cards available for the request.
79 | *
80 | * @param \Illuminate\Http\Request $request
81 | * @return array
82 | */
83 | public function cards(Request $request)
84 | {
85 | return [];
86 | }
87 |
88 | /**
89 | * Get the filters available for the resource.
90 | *
91 | * @param \Illuminate\Http\Request $request
92 | * @return array
93 | */
94 | public function filters(Request $request)
95 | {
96 | return [];
97 | }
98 |
99 | /**
100 | * Get the lenses available for the resource.
101 | *
102 | * @param \Illuminate\Http\Request $request
103 | * @return array
104 | */
105 | public function lenses(Request $request)
106 | {
107 | return [];
108 | }
109 |
110 | /**
111 | * Get the actions available for the resource.
112 | *
113 | * @param \Illuminate\Http\Request $request
114 | * @return array
115 | */
116 | public function actions(Request $request)
117 | {
118 | return [];
119 | }
120 |
121 | /***************************************************************************************
122 | ** HELPERS
123 | ***************************************************************************************/
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/Nova/Resource.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ## Checkout
7 |
8 | This package provides API endpoints and common functionality for cart, checkout, orders and coupons. You can use it with your own UI or use [Checkout Vue](https://github.com/64robots/checkout-vue) package that works with this API out of the box.
9 |
10 | ### Installation
11 |
12 | You can install this package via composer:
13 | ```
14 | composer require 64robots/checkout
15 | ```
16 | Once installed, this package will automatically register its service provider.
17 |
18 | You can publish the package migrations with:
19 | ```
20 | php artisan vendor:publish --provider="R64\Checkout\CheckoutServiceProvider" --tag="migrations"
21 | ```
22 |
23 | After the migrations have been published, you can create package tables with:
24 | ```
25 | php artisan migrate
26 | ```
27 | By running migrations, these tables will be created:
28 | - `customers`
29 | - `products`
30 | - `cart`
31 | - `cart_items`
32 | - `orders`
33 | - `order_items`
34 | - `coupons`
35 | - `order_purchases`
36 |
37 | You can also publish the package config with:
38 | ```
39 | php artisan vendor:publish --provider="R64\Checkout\CheckoutServiceProvider" --tag="config"
40 | ```
41 |
42 | After the config has been published, you can find it's contents in `config/checkout.php`
43 |
44 | ```php
45 | return [
46 |
47 | /*
48 | * Required parameters when submitting an order to /api/orders endpoint
49 | */
50 | 'required' => [
51 | 'customer_email',
52 | 'shipping_first_name',
53 | 'shipping_last_name',
54 | 'shipping_address_line1',
55 | // 'shipping_address_line2',
56 | 'shipping_address_city',
57 | 'shipping_address_region',
58 | 'shipping_address_zipcode',
59 | 'shipping_address_phone',
60 | 'billing_first_name',
61 | 'billing_last_name',
62 | 'billing_address_line1',
63 | // 'billing_address_line2',
64 | 'billing_address_city',
65 | 'billing_address_region',
66 | 'billing_address_zipcode',
67 | 'billing_address_phone',
68 | ],
69 |
70 | /*
71 | * Terms and conditions url used usually in the checkout UI
72 | */
73 | 'toc_url' => '#',
74 |
75 | /*
76 | * Currency code that will be saved with every order and symbol
77 | * that is usually used in the Cart, Checkout and Order UI
78 | */
79 | 'currency' => [
80 | 'code' => env('CHECKOUT_CURRENCY_CODE', 'USD'),
81 | 'symbol' => env('CHECKOUT_CURRENCY_SYMBOL', '$')
82 | ],
83 |
84 | /*
85 | * Percentage of Cart total and fixed fee will be stored for every
86 | * order purchase (transaction)
87 | */
88 | 'stripe' => [
89 | 'percentage_fee' => env('CHECKOUT_STRIPE_PERCENTAGE_FEE', 29 / 1000),
90 | 'fixed_fee' => env('CHECKOUT_STRIPE_FIXED_FEE', 30)
91 | ],
92 |
93 | /*
94 | * Shipping city and state is automatically resolved from zip code
95 | * using GeoNames service http://www.geonames.org/
96 | *
97 | * Country code constraints the search results
98 | * to specific country
99 | */
100 | 'geo_names' => [
101 | 'username' => env('CHECKOUT_GEO_NAMES_USERNAME', 'demo'),
102 | 'country_code' => env('CHECKOUT_GEO_NAMES_COUNTRY_CODE', 'US')
103 | ],
104 |
105 | /*
106 | * Class names can be replaced and extended with your own logic
107 | */
108 | 'product_model' => R64\Checkout\Models\Product::class,
109 | 'customer_model' => R64\Checkout\Models\Customer::class,
110 | 'cart_model' => R64\Checkout\Models\Cart::class,
111 | 'cart_item_model' => R64\Checkout\Models\CartItem::class,
112 | 'coupon_model' => R64\Checkout\Models\Coupon::class,
113 | 'order_model' => R64\Checkout\Models\Order::class,
114 | 'order_item_model' => R64\Checkout\Models\OrderItem::class,
115 | 'product_resource' => R64\Checkout\Http\Resources\ProductResource::class,
116 | 'payment' => R64\Checkout\StripePaymentHandler::class
117 | ];
118 | ```
119 |
120 | ### Nova
121 |
122 | You can publish nova resources with:
123 |
124 | ```
125 | php artisan vendor:publish --provider="R64\Checkout\CheckoutServiceProvider" --tag="nova"
126 | ```
127 |
128 | ### Available API Endpoints
129 |
130 | Once you install the package and run migrations, these API endpoints will be available in your application.
131 |
132 | [API Docs](https://github.com/64robots/checkout/wiki/API-Endpoints)
133 |
134 | ## Licence
135 |
136 | Checkout is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT)
137 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "64robots/checkout",
3 | "description": "Checkout Package.",
4 | "keywords": ["framework", "laravel", "checkout"],
5 | "license": "MIT",
6 | "type": "library",
7 | "require": {
8 | "64robots/stripe": "*",
9 | "axlon/laravel-postal-code-validation": "^3.0",
10 | "doctrine/dbal": "^2.12",
11 | "guzzlehttp/guzzle": "^7.0.1",
12 | "illuminate/support": "^8.71",
13 | "illuminate/database": "^8.71",
14 | "laravel/framework": "^8.0",
15 | "paypal/paypal-checkout-sdk": "1.0.1",
16 | "php": "^7.1.3 || ^8.0"
17 | },
18 | "require-dev": {
19 | "filp/whoops": "^2.0",
20 | "phpunit/phpunit": "^8.3",
21 | "orchestra/testbench": "^6.23"
22 | },
23 | "scripts" : {
24 | "test" : "vendor/bin/phpunit"
25 | },
26 | "autoload": {
27 | "files": [
28 | "src/Helpers/Globals.php"
29 | ],
30 | "psr-4": {
31 | "R64\\Checkout\\": "src",
32 | "R64\\Checkout\\Database\\Factories\\": "database/factories"
33 | }
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "R64\\Checkout\\Tests\\": "tests"
38 | }
39 | },
40 | "repositories": [
41 | {
42 | "type": "vcs",
43 | "url": "https://github.com/64robots/stripe"
44 | }
45 | ],
46 | "extra": {
47 | "laravel": {
48 | "providers": [
49 | "R64\\Checkout\\CheckoutServiceProvider"
50 | ]
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/config/checkout.php:
--------------------------------------------------------------------------------
1 | [
9 | 'customer_email',
10 | 'shipping_first_name',
11 | 'shipping_last_name',
12 | 'shipping_address_line1',
13 | // 'shipping_address_line2',
14 | 'shipping_address_city',
15 | 'shipping_address_region',
16 | 'shipping_address_zipcode',
17 | 'shipping_address_phone',
18 | 'billing_first_name',
19 | 'billing_last_name',
20 | 'billing_address_line1',
21 | // 'billing_address_line2',
22 | 'billing_address_city',
23 | 'billing_address_region',
24 | 'billing_address_zipcode',
25 | 'billing_address_phone',
26 | ],
27 |
28 | /*
29 | * Terms and conditions url used usually in the checkout UI
30 | */
31 | 'toc_url' => '#',
32 |
33 | /*
34 | * Currency code that will be saved with every order and symbol
35 | * that is usually used in the Cart, Checkout and Order UI
36 | */
37 | 'currency' => [
38 | 'code' => env('CHECKOUT_CURRENCY_CODE', 'USD'),
39 | 'symbol' => env('CHECKOUT_CURRENCY_SYMBOL', '$')
40 | ],
41 |
42 | /*
43 | * Percentage of Cart total and fixed fee will be stored for every
44 | * order purchase (transaction)
45 | */
46 | 'stripe' => [
47 | 'percentage_fee' => env('CHECKOUT_STRIPE_PERCENTAGE_FEE', 2.9 / 100),
48 | 'fixed_fee' => env('CHECKOUT_STRIPE_FIXED_FEE', 30)
49 | ],
50 |
51 | /*
52 | * Shipping city and state is automatically resolved from zip code
53 | * using GeoNames service http://www.geonames.org/
54 | *
55 | * Country code constraints the search results
56 | * to specific country
57 | */
58 | 'geo_names' => [
59 | 'username' => env('CHECKOUT_GEO_NAMES_USERNAME', 'demo'),
60 | 'country_code' => env('CHECKOUT_GEO_NAMES_COUNTRY_CODE', 'US')
61 | ],
62 |
63 | /*
64 | * Class names can be replaced and extended with your own logic
65 | */
66 | 'product_model' => R64\Checkout\Models\Product::class,
67 | 'customer_model' => R64\Checkout\Models\Customer::class,
68 | 'cart_model' => R64\Checkout\Models\Cart::class,
69 | 'cart_item_model' => R64\Checkout\Models\CartItem::class,
70 | 'coupon_model' => R64\Checkout\Models\Coupon::class,
71 | 'order_model' => R64\Checkout\Models\Order::class,
72 | 'order_item_model' => R64\Checkout\Models\OrderItem::class,
73 | 'product_resource' => R64\Checkout\Http\Resources\ProductResource::class,
74 | 'stripe_payment' => R64\Checkout\StripePaymentHandler::class,
75 | 'paypal_payment' => R64\Checkout\PaypalPaymentHandler::class
76 | ];
77 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 |
--------------------------------------------------------------------------------
/database/factories/CartFactory.php:
--------------------------------------------------------------------------------
1 | Customer::factory()->create()->id,
22 | 'items_subtotal' => $this->faker->numberBetween(100, 200),
23 | 'total' => $this->faker->numberBetween(400, 1000),
24 | 'ip_address' => $this->faker->ipv4,
25 | 'token' => Token::generate()->toString(),
26 | 'shipping' => $this->faker->numberBetween(1000, 10000),
27 | 'customer_email' => $this->faker->email,
28 | 'shipping_first_name' => $this->faker->firstName,
29 | 'shipping_last_name' => $this->faker->lastName,
30 | 'shipping_address_line1' => $this->faker->streetAddress,
31 | 'shipping_address_line2' => $this->faker->secondaryAddress,
32 | 'shipping_address_city' => $this->faker->city,
33 | 'shipping_address_region' => $this->faker->state,
34 | 'shipping_address_zipcode' => $this->faker->postcode,
35 | 'shipping_address_phone' => $this->faker->phoneNumber,
36 | 'billing_first_name' => $this->faker->firstName,
37 | 'billing_last_name' => $this->faker->lastName,
38 | 'billing_address_line1' => $this->faker->streetAddress,
39 | 'billing_address_line2' => $this->faker->secondaryAddress,
40 | 'billing_address_city' => $this->faker->city,
41 | 'billing_address_region' => $this->faker->state,
42 | 'billing_address_zipcode' => $this->faker->postcode,
43 | 'billing_address_phone' => $this->faker->phoneNumber
44 | ];
45 | }
46 |
47 | public function withProducts()
48 | {
49 | return $this->afterCreating(function (Cart $cart) {
50 | CartItem::factory()->create([
51 | 'cart_id' => $cart->id
52 | ]);
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/database/factories/CartItemFactory.php:
--------------------------------------------------------------------------------
1 | function () {
24 | return Cart::factory()->create()->id;
25 | },
26 | 'product_id' => function () {
27 | return Product::factory()->create()->id;
28 | },
29 | 'price' => $this->faker->numberBetween(200, 400),
30 | 'quantity' => $this->faker->numberBetween(1, 10),
31 | 'token' => Token::generate(),
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/factories/CouponFactory.php:
--------------------------------------------------------------------------------
1 | faker->numberBetween(0, 2);
17 | return [
18 | 'name' => ['$10 off', '$20 off', '$30 off', '10% off', 'FREE'][$randomNumber],
19 | 'code' => ['$10OFF', '$20OFF', '$30OFF', '10%OFF', 'FREE'][$randomNumber],
20 | 'discount' => [1000, 2000, 3000, 1000, 10000][$randomNumber],
21 | 'percentage' => [false, false, false, true, true][$randomNumber],
22 | 'active' => true
23 | ];
24 | }
25 |
26 | public function tenDollarsOff()
27 | {
28 | return $this->state([
29 | 'name' => '$10 off',
30 | 'code' => '$10OFF',
31 | 'discount' => 1000,
32 | 'percentage' => false
33 | ]);
34 | }
35 |
36 | public function twentyDollarsOff()
37 | {
38 | return $this->state([
39 | 'name' => '$20 off',
40 | 'code' => '$20OFF',
41 | 'discount' => 2000,
42 | 'percentage' => false
43 | ]);
44 | }
45 |
46 | public function thirtyDollarsOf()
47 | {
48 | return $this->state([
49 | 'name' => '$30 off',
50 | 'code' => '$30OFF',
51 | 'discount' => 3000,
52 | 'percentage' => false
53 | ]);
54 | }
55 |
56 | public function tenPercentOff()
57 | {
58 | return $this->state([
59 | 'name' => '10% off',
60 | 'code' => '10%OFF',
61 | 'discount' => 1000,
62 | 'percentage' => true
63 | ]);;
64 | }
65 |
66 | public function free()
67 | {
68 | return $this->state([
69 | 'name' => 'free',
70 | 'code' => 'free',
71 | 'discount' => 10000,
72 | 'percentage' => true
73 | ]);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/database/factories/CustomerFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->firstName,
20 | 'last_name' => $this->faker->lastName,
21 | 'email' => $this->faker->unique()->safeEmail,
22 | 'email_verified_at' => now(),
23 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
24 | 'remember_token' => Str::random(10),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/factories/OrderFactory.php:
--------------------------------------------------------------------------------
1 | function () {
18 | return \R64\Checkout\Models\Cart::factory()->create()->id;
19 | },
20 | 'items_total' => $this->faker->numberBetween(1000, 10000),
21 | 'customer_email' => $this->faker->unique()->safeEmail,
22 | 'shipping_first_name' => $this->faker->catchPhrase,
23 | 'shipping_last_name' => $this->faker->catchPhrase,
24 | 'shipping_address_line1' => $this->faker->catchPhrase,
25 | 'shipping_address_line2' => $this->faker->catchPhrase,
26 | 'shipping_address_city' => $this->faker->catchPhrase,
27 | 'shipping_address_region' => $this->faker->catchPhrase,
28 | 'shipping_address_zipcode' => $this->faker->catchPhrase,
29 | 'shipping_address_phone' => $this->faker->catchPhrase,
30 | 'billing_address_line1' => $this->faker->catchPhrase,
31 | 'billing_address_line2' => $this->faker->catchPhrase,
32 | 'billing_address_city' => $this->faker->catchPhrase,
33 | 'billing_address_region' => $this->faker->catchPhrase,
34 | 'billing_address_zipcode' => $this->faker->catchPhrase,
35 | 'billing_address_phone' => $this->faker->catchPhrase,
36 | 'status' => $this->faker->catchPhrase,
37 | 'customer_notes' => $this->faker->text,
38 | 'admin_notes' => $this->faker->text,
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/database/factories/OrderItemFactory.php:
--------------------------------------------------------------------------------
1 | function () {
18 | return \R64\Checkout\Models\Product::factory()->create()->id;
19 | },
20 | 'cart_item_id' => function () {
21 | return \R64\Checkout\Models\CartItem::factory()->create()->id;
22 | },
23 | 'name' => $this->faker->word,
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/factories/ProductFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->numberBetween(100, 400),
16 | 'name' => $this->faker->name
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_1_create_products_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->integer('price');
19 | $table->string('name');
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists('products');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_2_create_customers_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('first_name');
19 | $table->string('last_name');
20 | $table->string('email')->unique();
21 | $table->timestamp('email_verified_at')->nullable();
22 | $table->string('password');
23 | $table->rememberToken();
24 | $table->softDeletes();
25 | $table->timestamps();
26 | });
27 | }
28 |
29 | /**
30 | * Reverse the migrations.
31 | *
32 | * @return void
33 | */
34 | public function down()
35 | {
36 | Schema::dropIfExists('customers');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_3_create_coupons_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('name');
19 | $table->string('code')->unique();
20 | $table->unsignedInteger('discount');
21 | $table->boolean('percentage')->default(false);
22 | $table->boolean('active')->default(false);
23 | $table->timestamps();
24 | $table->softDeletes();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('coupons');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_4_create_carts_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 |
19 | $table->unsignedBigInteger('customer_id')->nullable();
20 | $table->unsignedBigInteger('coupon_id')->nullable();
21 |
22 | $table->integer('items_subtotal')->default(0);
23 | $table->integer('tax_rate')->default(0);
24 | $table->integer('tax')->default(0);
25 | $table->integer('total')->default(0);
26 | $table->integer('discount')->default(0);
27 | $table->integer('shipping')->default(0);
28 | $table->string('customer_email')->nullable();
29 | $table->string('shipping_first_name')->nullable();
30 | $table->string('shipping_last_name')->nullable();
31 | $table->string('shipping_address_line1')->nullable();
32 | $table->string('shipping_address_line2')->nullable();
33 | $table->string('shipping_address_city')->nullable();
34 | $table->string('shipping_address_region')->nullable();
35 | $table->string('shipping_address_zipcode')->nullable();
36 | $table->string('shipping_address_phone')->nullable();
37 | $table->boolean('billing_same')->default(true);
38 | $table->string('billing_first_name')->nullable();
39 | $table->string('billing_last_name')->nullable();
40 | $table->string('billing_address_line1')->nullable();
41 | $table->string('billing_address_line2')->nullable();
42 | $table->string('billing_address_city')->nullable();
43 | $table->string('billing_address_region')->nullable();
44 | $table->string('billing_address_zipcode')->nullable();
45 | $table->string('billing_address_phone')->nullable();
46 | $table->text('customer_notes')->nullable();
47 | $table->string('token')->unique();
48 | $table->ipAddress('ip_address');
49 | $table->timestamps();
50 | $table->softDeletes();
51 |
52 | $table->foreign('customer_id')->references('id')->on('customers');
53 | $table->foreign('coupon_id')->references('id')->on('coupons');
54 | });
55 | }
56 |
57 | /**
58 | * Reverse the migrations.
59 | *
60 | * @return void
61 | */
62 | public function down()
63 | {
64 | Schema::dropIfExists('carts');
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_5_create_cart_items_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('cart_id');
19 | $table->unsignedBigInteger('product_id');
20 | $table->unsignedInteger('discount')->default(0);
21 | $table->string('customer_note')->nullable();
22 | $table->integer('price');
23 | $table->integer('quantity');
24 | $table->string('token');
25 | $table->timestamps();
26 | $table->softDeletes();
27 |
28 | $table->foreign('cart_id')->references('id')->on('carts');
29 | $table->foreign('product_id')->references('id')->on('products');
30 | });
31 | }
32 |
33 | /**
34 | * Reverse the migrations.
35 | *
36 | * @return void
37 | */
38 | public function down()
39 | {
40 | Schema::dropIfExists('cart_items');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_6_create_orders_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 |
19 | $table->unsignedBigInteger('cart_id')->nullable();
20 | $table->unsignedBigInteger('customer_id')->nullable();
21 | $table->unsignedBigInteger('coupon_id')->nullable();
22 |
23 | $table->string('order_number')->nullable();
24 | $table->integer('items_total');
25 | $table->unsignedInteger('shipping');
26 | $table->integer('tax');
27 | $table->integer('total');
28 | $table->integer('tax_rate')->nullable();
29 | $table->unsignedInteger('discount')->default(0);
30 | $table->string('customer_email');
31 | $table->string('shipping_first_name')->nullable();
32 | $table->string('shipping_last_name')->nullable();
33 | $table->string('shipping_address_line1')->nullable();
34 | $table->string('shipping_address_line2')->nullable();
35 | $table->string('shipping_address_city')->nullable();
36 | $table->string('shipping_address_region')->nullable();
37 | $table->string('shipping_address_zipcode')->nullable();
38 | $table->string('shipping_address_phone')->nullable();
39 | $table->string('billing_first_name')->nullable();
40 | $table->string('billing_last_name')->nullable();
41 | $table->string('billing_address_line1')->nullable();
42 | $table->string('billing_address_line2')->nullable();
43 | $table->string('billing_address_city')->nullable();
44 | $table->string('billing_address_region')->nullable();
45 | $table->string('billing_address_zipcode')->nullable();
46 | $table->string('billing_address_phone')->nullable();
47 | $table->string('status')->nullable();
48 | $table->text('customer_notes')->nullable();
49 | $table->text('admin_notes')->nullable();
50 | $table->string('token')->unique();
51 | $table->string('currency');
52 | $table->timestamps();
53 | $table->softDeletes();
54 |
55 | $table->foreign('cart_id')->references('id')->on('carts');
56 | $table->foreign('customer_id')->references('id')->on('customers');
57 | $table->foreign('coupon_id')->references('id')->on('coupons');
58 | });
59 | }
60 |
61 | /**
62 | * Reverse the migrations.
63 | *
64 | * @return void
65 | */
66 | public function down()
67 | {
68 | Schema::dropIfExists('orders');
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_7_create_order_items_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('order_id')->nullable();
19 | $table->unsignedBigInteger('product_id')->nullable();
20 | $table->unsignedBigInteger('cart_item_id')->nullable();
21 | $table->integer('price');
22 | $table->integer('quantity');
23 | $table->string('name');
24 | $table->string('customer_note')->nullable();
25 | $table->timestamps();
26 | $table->softDeletes();
27 |
28 | $table->index('product_id');
29 | $table->foreign('product_id')->references('id')->on('products');
30 | $table->foreign('cart_item_id')->references('id')->on('cart_items');
31 | $table->foreign('order_id')->references('id')->on('orders');
32 | });
33 | }
34 |
35 | /**
36 | * Reverse the migrations.
37 | *
38 | * @return void
39 | */
40 | public function down()
41 | {
42 | Schema::dropIfExists('order_items');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/database/migrations/2020_02_17_8_create_order_purchases_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('order_id')->nullable();
19 |
20 | $table->unsignedBigInteger('customer_id')->nullable();
21 | $table->text('order_data');
22 | $table->string('email');
23 | $table->integer('amount');
24 | $table->string('card_type')->nullable();
25 | $table->string('card_last4')->nullable();
26 | $table->string('stripe_customer_id')->nullable();
27 | $table->string('stripe_charge_id')->nullable();
28 | $table->string('stripe_card_id')->nullable();
29 | $table->integer('stripe_fee')->default(0);
30 |
31 | $table->timestamps();
32 | $table->softDeletes();
33 |
34 | $table->foreign('order_id')->references('id')->on('orders');
35 | $table->foreign('customer_id')->references('id')->on('customers');
36 | });
37 | }
38 |
39 | /**
40 | * Reverse the migrations.
41 | *
42 | * @return void
43 | */
44 | public function down()
45 | {
46 | Schema::dropIfExists('order_purchases');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/database/migrations/2020_03_24_9_add_options_to_carts_and_orders_table.php:
--------------------------------------------------------------------------------
1 | text('options')->nullable();
18 | });
19 |
20 | Schema::table('orders', function (Blueprint $table) {
21 | $table->text('options')->nullable();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | *
28 | * @return void
29 | */
30 | public function down()
31 | {
32 | Schema::table('carts', function (Blueprint $table) {
33 | $table->dropColumn('options');
34 | });
35 |
36 | Schema::table('orders', function (Blueprint $table) {
37 | $table->dropColumn('options');
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/database/migrations/2020_06_03_10_add_paypal_columns_to_order_purchases_table.php:
--------------------------------------------------------------------------------
1 | string('payment_processor')->default(PaymentHandlerFactory::STRIPE);
19 | $table->string('paypal_order_id')->nullable();
20 | $table->string('paypal_authorization_id')->nullable();
21 | $table->string('paypal_capture_id')->nullable();
22 | $table->string('paypal_payer_id')->nullable();
23 | $table->unsignedInteger('paypal_fee')->default(0);
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::table('order_purchases', function (Blueprint $table) {
35 | $table->dropColumn([
36 | 'payment_processor',
37 | 'paypal_order_id',
38 | 'paypal_authorization_id',
39 | 'paypal_capture_id',
40 | 'paypal_payer_id',
41 | 'paypal_fee'
42 | ]);
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/routes/api_guest.php:
--------------------------------------------------------------------------------
1 | group(function () {
7 | /***************************************************************************************
8 | ** Cart
9 | ***************************************************************************************/
10 |
11 | Route::get('carts/{cart}', 'CartController@get');
12 | Route::post('carts', 'CartController@create');
13 | Route::put('carts/{cart}', 'CartController@update');
14 | Route::delete('carts/{cart}', 'CartController@delete');
15 |
16 | Route::put('carts/{cart}/zipcode', 'CartZipCodeController@update');
17 | Route::put('carts/{cart}/shipping', 'CartShippingController@update');
18 | Route::put('carts/{cart}/email', 'CartEmailController@update');
19 | Route::post('carts/{cart}/options', 'CartOptionsController@store');
20 |
21 | Route::put('carts/{cart}/coupon-code', 'CartCouponController@update');
22 | Route::delete('carts/{cart}/coupon-code', 'CartCouponController@delete');
23 |
24 | /***************************************************************************************
25 | ** Cart Items
26 | ***************************************************************************************/
27 |
28 | Route::post('carts/{cart}/cart-items', 'CartItemController@create');
29 | Route::put('cart-items/{cartItem}', 'CartItemController@update');
30 | Route::delete('cart-items/{cartItem}', 'CartItemController@delete');
31 |
32 | /***************************************************************************************
33 | ** Checkout
34 | ***************************************************************************************/
35 |
36 | Route::get('checkout/settings', 'CheckoutSettingsController@index');
37 |
38 | /***************************************************************************************
39 | ** Orders
40 | ***************************************************************************************/
41 | Route::get('orders/{order}', 'OrderController@get');
42 | Route::post('orders', 'OrderController@create');
43 | });
44 |
45 |
--------------------------------------------------------------------------------
/src/CheckoutFields.php:
--------------------------------------------------------------------------------
1 | intersectByKeys($availableFields)->all();
34 |
35 | return collect($availableFields)->merge($configFields)->all();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/ConfigurableModel.php:
--------------------------------------------------------------------------------
1 | model = $model;
19 | }
20 |
21 | /**
22 | * @return Model
23 | */
24 | public function getModel()
25 | {
26 | return $this->model;
27 | }
28 |
29 | public function getClassName()
30 | {
31 | return get_class($this->getModel());
32 | }
33 |
34 | public function getTableName()
35 | {
36 | return $this->getModel()->getTable();
37 | }
38 |
39 | public function getForeignKey()
40 | {
41 | return Str::singular($this->getTableName()) . '_id';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Contracts/Cart.php:
--------------------------------------------------------------------------------
1 | order = $order;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Events/NewOrderPurchase.php:
--------------------------------------------------------------------------------
1 | order = $order;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Facades/AddressSearch.php:
--------------------------------------------------------------------------------
1 | email = $email;
18 | $this->firstName = $firstName;
19 | $this->lastName = $lastName;
20 | }
21 |
22 | public function getId()
23 | {
24 | return null;
25 | }
26 |
27 | public function getFirstName()
28 | {
29 | return $this->firstName;
30 | }
31 |
32 | public function getLastName()
33 | {
34 | return $this->lastName;
35 | }
36 |
37 | public function getEmail()
38 | {
39 | return $this->email;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Helpers/Address/Address.php:
--------------------------------------------------------------------------------
1 | stateName;
23 | }
24 |
25 | /**
26 | * @param mixed $stateName
27 | * @return Address
28 | */
29 | public function setStateName($stateName)
30 | {
31 | $this->stateName = $stateName;
32 |
33 | return $this;
34 | }
35 |
36 | /**
37 | * @return mixed
38 | */
39 | public function getStateCode()
40 | {
41 | return $this->stateCode;
42 | }
43 |
44 | /**
45 | * @param mixed $stateCode
46 | * @return Address
47 | */
48 | public function setStateCode($stateCode)
49 | {
50 | $this->stateCode = $stateCode;
51 |
52 | return $this;
53 | }
54 |
55 | /**
56 | * @return mixed
57 | */
58 | public function getCityName()
59 | {
60 | return $this->cityName;
61 | }
62 |
63 | /**
64 | * @param mixed $cityName
65 | * @return Address
66 | */
67 | public function setCityName($cityName)
68 | {
69 | $this->cityName = $cityName;
70 |
71 | return $this;
72 | }
73 |
74 | /**
75 | * @return mixed
76 | */
77 | public function getLatitude()
78 | {
79 | return $this->latitude;
80 | }
81 |
82 | /**
83 | * @param mixed $latitude
84 | * @return Address
85 | */
86 | public function setLatitude($latitude)
87 | {
88 | $this->latitude = $latitude;
89 |
90 | return $this;
91 | }
92 |
93 | /**
94 | * @return mixed
95 | */
96 | public function getLongitude()
97 | {
98 | return $this->longitude;
99 | }
100 |
101 | /**
102 | * @param mixed $longitude
103 | * @return Address
104 | */
105 | public function setLongitude($longitude)
106 | {
107 | $this->longitude = $longitude;
108 |
109 | return $this;
110 | }
111 |
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/Helpers/Address/AddressSearch.php:
--------------------------------------------------------------------------------
1 | geoNames = $geoNames;
18 | }
19 |
20 | public function getByPostalCode($postalCode)
21 | {
22 | try {
23 | return collect($this->geoNames->postalCodeLookup($postalCode))
24 | ->map(function ($address) {
25 | return (new Address())
26 | ->setCityName($address['placeName'])
27 | ->setStateName($address['adminName1'])
28 | ->setStateCode($address['adminCode1'])
29 | ->setLatitude($address['lat'])
30 | ->setLongitude($address['lng']);
31 |
32 | });
33 | } catch (RuntimeException $e) {
34 | //
35 | }
36 |
37 | return collect();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Helpers/Address/FakeGeoNames.php:
--------------------------------------------------------------------------------
1 | 'Baltimore',
12 | 'adminName1' => 'Maryland',
13 | 'adminCode1' => 'MD',
14 | 'lat' => 39.293810,
15 | 'lng' => -76.562940
16 | ]
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Helpers/Address/GeoNames.php:
--------------------------------------------------------------------------------
1 | client = $client;
27 | $this->username = $username;
28 | $this->countryCode = $countryCode;
29 | }
30 |
31 | public function postalCodeLookup($postalCode)
32 | {
33 | $url = sprintf(
34 | "http://api.geonames.org/postalCodeLookupJSON?postalcode=%s&country=%s&username=%s",
35 | $postalCode,
36 | $this->countryCode,
37 | $this->username
38 | );
39 |
40 | try {
41 | $response = $this->client->get($url);
42 | } catch (ClientException $e) {
43 | throw new \RuntimeException($e->getMessage());
44 | }
45 |
46 | $response = json_decode($response->getBody(), true);
47 |
48 | if (isset($response['status'])) {
49 | throw new \RuntimeException($response['status']['message']);
50 | }
51 |
52 | if (isset($response['postalcodes'])) {
53 | return $response['postalcodes'];
54 | }
55 |
56 | throw new \RuntimeException(var_export($response, true));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Helpers/Globals.php:
--------------------------------------------------------------------------------
1 | $property;
8 | }
9 | return $failedReturn;
10 | }
11 | }
12 |
13 | if (!function_exists('jsonEncodeDecode')) {
14 | function jsonEncodeDecode($data)
15 | {
16 | return json_decode(json_encode($data));
17 | }
18 | }
19 |
20 | if (!function_exists('getPercent')) {
21 | function getPercent(int $value)
22 | {
23 | return round($value * 100, 1);
24 | }
25 | }
26 |
27 | if (!function_exists('displayMoney')) {
28 | function displayMoney(int $value)
29 | {
30 | return number_format($value / 100, 2);
31 | }
32 | }
33 |
34 | if (!function_exists('displayTaxRate')) {
35 | function displayTaxRate($value)
36 | {
37 | return number_format(!is_null($value) ? $value / 100 : 0, 2);
38 | }
39 | }
40 |
41 | if (!function_exists('subdomainUrl')) {
42 | function subdomainUrl(string $subdomain)
43 | {
44 | $http = config('app.env') == 'local' ? 'http://' : 'https://';
45 | return $http . $subdomain . '.' . env('APP_BASE_URL');
46 | }
47 | }
48 |
49 | /**
50 | * Strip out and lowercase hashtags
51 | */
52 | if (!function_exists('normalize_tag')) {
53 | function normalize_tag($tag)
54 | {
55 | $normalized = ltrim($tag, '#');
56 | $normalized = mb_strtolower($normalized);
57 |
58 | return $normalized;
59 | }
60 | }
61 |
62 | /**
63 | * Check if user authenticated and he's an admin.
64 | */
65 | if (!function_exists('isAdmin')) {
66 | function isAdmin()
67 | {
68 | return (bool) optional(auth()->user())->is_admin;
69 | }
70 | }
71 |
72 | /**
73 | * Check if user is not admin.
74 | */
75 | if (!function_exists('isNotAdmin')) {
76 | function isNotAdmin()
77 | {
78 | return !isAdmin();
79 | }
80 | }
81 |
82 | /**
83 | * Generate an unique key based on given keys.
84 | * Keys can be passed both as an array or as arguments.
85 | */
86 | if (!function_exists('generateUniqueKey')) {
87 | function generateUniqueKey(): string
88 | {
89 | // Transform key to an array of keys if it's not an array yet.
90 | $keys = is_array(func_get_args()[0]) ? func_get_args()[0] : func_get_args();
91 |
92 | return 'unq_' . implode('_', $keys);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Helpers/HashTagExtractor.php:
--------------------------------------------------------------------------------
1 | text = $text;
12 | }
13 |
14 | public function tags() : array
15 | {
16 | return $this->getProcessed();
17 | }
18 |
19 | private function getProcessed() : array
20 | {
21 | if (is_null($this->processed)) {
22 | preg_match_all($this->getMatcherRules(), $this->text, $matches);
23 |
24 | $this->processed = $matches[0];
25 | }
26 |
27 | return $this->processed;
28 | }
29 |
30 | private function getMatcherRules()
31 | {
32 | return "/\#[a-zA-Z]+/";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Helpers/Price.php:
--------------------------------------------------------------------------------
1 | json([
9 | 'success' => true,
10 | 'data' => $data,
11 | 'message' => $message,
12 | ]);
13 | }
14 |
15 | public static function error($data = [], string $error = '', $responseCode = 400)
16 | {
17 | return response()->json([
18 | 'success' => false,
19 | 'error' => $error,
20 | 'data' => $data,
21 | ], $responseCode);
22 | }
23 |
24 | public static function noJsonSuccess($data = [], string $message = '')
25 | {
26 | return [
27 | 'success' => true,
28 | 'data' => $data,
29 | 'message' => $message,
30 | ];
31 | }
32 |
33 | public static function noJsonError($data = [], string $error = '')
34 | {
35 | return [
36 | 'success' => false,
37 | 'error' => $error,
38 | 'data' => $data,
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Helpers/Search.php:
--------------------------------------------------------------------------------
1 | term = $term;
28 |
29 | // Ordering
30 | $this->order_field = Arr::get($additional_params, 'order_field');
31 | $this->order_direction = Arr::get($additional_params, 'order_direction', 'ASC');
32 |
33 | // Limiting (Pagination Off)
34 | $this->limit = Arr::get($additional_params, 'limit', 0);
35 |
36 | // Pagination
37 | $this->paginate = Arr::get($additional_params, 'paginate', false);
38 | $this->items_per_page = Arr::get($additional_params, 'items_per_page', 10);
39 |
40 | // additional params
41 | $this->params = $additional_params;
42 | $this->user = $user;
43 | }
44 |
45 | /***************************************************************************************
46 | ** USERS
47 | ***************************************************************************************/
48 |
49 | public function users()
50 | {
51 | $users = User::when($this->term, function ($query) {
52 | $query->whereRaw($this->getRawConcatStatement($this->term))
53 | ->orWhere('email', 'LIKE', '%' . $this->term . '%');
54 | })
55 | ->when($this->order_field, function ($query) {
56 | $query->orderBy($this->order_field, $this->order_direction);
57 | })
58 | ->when(!$this->paginate && $this->limit, function ($query) {
59 | $query->take($this->limit);
60 | })
61 | ->select('users.*');
62 | // pagination
63 | if ($this->paginate && $this->items_per_page) {
64 | return $users->paginate($this->items_per_page);
65 | }
66 | return $users->get();
67 | }
68 |
69 | /***************************************************************************************
70 | ** HELPERS
71 | ***************************************************************************************/
72 |
73 | protected function getRawConcatStatement($term)
74 | {
75 | if (app()->environment('testing')) {
76 | return '(`users`.`first_name` || `users`.`last_name`) LIKE "%' . $term . '%"';
77 | }
78 | return 'CONCAT(`users`.`first_name`, " ", `users`.`last_name`) LIKE "%' . $term . '%"';
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Helpers/Token.php:
--------------------------------------------------------------------------------
1 | getCommand('GetObject', [
26 | 'Bucket' => env('AWS_BUCKET'),
27 | 'Key' => $key
28 | ]);
29 |
30 | $request = $s3->createPresignedRequest($cmd, '+20 minutes');
31 |
32 | return (string) $request->getUri();
33 | }
34 |
35 | public static function duplicateS3File(string $source_key, string $destination_folder)
36 | {
37 | // generate new file's key
38 | $new_key = $destination_folder . '/' . Str::random(40);
39 |
40 | $s3 = AWS::createClient('s3');
41 | $s3->copyObject([
42 | 'Bucket' => env('AWS_BUCKET'),
43 | 'Key' => $new_key,
44 | 'CopySource' => env('AWS_BUCKET') . '/' . $source_key,
45 | ]);
46 | return $new_key;
47 | }
48 |
49 | public static function getCopyName(string $original)
50 | {
51 | // check if a string ends with a number in parentheses
52 | $matches = [];
53 | if (!preg_match('#(\(\d+\))$#', $original, $matches)) {
54 | return $original . ' (1)';
55 | }
56 |
57 | // try to get the number and increment it
58 | $number = 0;
59 | $number_matches = [];
60 | if (preg_match('#(\d+)#', $matches[1], $number_matches)) {
61 | $number = $number_matches[1];
62 | $number = (int) $number;
63 | $number++;
64 | }
65 |
66 | // strip out the old number and replace with new in parentheses
67 | $new_string = Str::lreplace($matches[1], '', $original);
68 | $new_string = $new_string . '(' . $number . ')';
69 | return $new_string;
70 | }
71 |
72 | public static function getSantizedName(UploadedFile $file, bool $titleCase = false)
73 | {
74 | // get original name w/o extension
75 | $name = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
76 |
77 | // remove anything but Letters, Numbers, and Dashes
78 | $name = preg_replace('/[^0-9a-zA-Z\-\s]/', ' ', $name);
79 |
80 | // uppercase first letter, remove dashes
81 | if ($titleCase) {
82 | $name = Str::replace('-', ' ', $name);
83 | $name = title_case($name);
84 | }
85 | return $name;
86 | }
87 |
88 | public static function uniqueCode(int $length = 8, bool $all_caps = false, string $check_table = null, string $check_column = null)
89 | {
90 | // 1. generate code
91 | $code = self::generateCode($length, $all_caps);
92 |
93 | // 2. validate unique
94 | if ($check_table && $check_column) {
95 | $validator = Validator::make(['code' => $code], [
96 | 'code' => 'unique:' . $check_table . ',' . $check_column]
97 | );
98 | // if fails run again
99 | if ($validator->fails()) {
100 | return self::uniqueCode($length, $all_caps, $check_table, $check_column);
101 | }
102 | }
103 | return $code;
104 | }
105 |
106 | public static function generateCode(int $length = 8, bool $all_caps = false)
107 | {
108 | // set characters
109 | $pool = array_merge(range(0,9), range('A', 'Z'));
110 | if (!$all_caps) {
111 | $pool = range('a', 'z');
112 | }
113 |
114 | // generate code
115 | $code = '';
116 | for($i=0; $i < $length; $i++) {
117 | $code .= $pool[mt_rand(0, count($pool) - 1)];
118 | }
119 | return $code;
120 | }
121 |
122 | public static function uniqueNumber(int $length = 8, string $check_table = null, string $check_column = null)
123 | {
124 | // 1. unique number
125 | $number = randomNumber($length);
126 |
127 | // 2. validate unique
128 | if ($check_table && $check_column) {
129 | $validator = Validator::make(['code' => $number], [
130 | 'code' => 'unique:' . $check_table . ',' . $check_column]
131 | );
132 | // if fails run again
133 | if ($validator->fails()) {
134 | return self::uniqueNumber($length, $check_table, $check_column);
135 | }
136 | }
137 | return $number;
138 | }
139 |
140 | /***************************************************************************************
141 | ** FORMS
142 | ***************************************************************************************/
143 |
144 | public static function formatPhoneNumber(string $phone)
145 | {
146 | return "(" . substr($phone, 0, 3) . ") " . substr($phone, 3, 3) . "-" . substr($phone, 6);
147 | }
148 |
149 | public static function states()
150 | {
151 | return [
152 | 'AL'=>'Alabama',
153 | 'AK'=>'Alaska',
154 | 'AZ'=>'Arizona',
155 | 'AR'=>'Arkansas',
156 | 'CA'=>'California',
157 | 'CO'=>'Colorado',
158 | 'CT'=>'Connecticut',
159 | 'DE'=>'Delaware',
160 | 'DC'=>'District of Columbia',
161 | 'FL'=>'Florida',
162 | 'GA'=>'Georgia',
163 | 'HI'=>'Hawaii',
164 | 'ID'=>'Idaho',
165 | 'IL'=>'Illinois',
166 | 'IN'=>'Indiana',
167 | 'IA'=>'Iowa',
168 | 'KS'=>'Kansas',
169 | 'KY'=>'Kentucky',
170 | 'LA'=>'Louisiana',
171 | 'ME'=>'Maine',
172 | 'MD'=>'Maryland',
173 | 'MA'=>'Massachusetts',
174 | 'MI'=>'Michigan',
175 | 'MN'=>'Minnesota',
176 | 'MS'=>'Mississippi',
177 | 'MO'=>'Missouri',
178 | 'MT'=>'Montana',
179 | 'NE'=>'Nebraska',
180 | 'NV'=>'Nevada',
181 | 'NH'=>'New Hampshire',
182 | 'NJ'=>'New Jersey',
183 | 'NM'=>'New Mexico',
184 | 'NY'=>'New York',
185 | 'NC'=>'North Carolina',
186 | 'ND'=>'North Dakota',
187 | 'OH'=>'Ohio',
188 | 'OK'=>'Oklahoma',
189 | 'OR'=>'Oregon',
190 | 'PA'=>'Pennsylvania',
191 | 'RI'=>'Rhode Island',
192 | 'SC'=>'South Carolina',
193 | 'SD'=>'South Dakota',
194 | 'TN'=>'Tennessee',
195 | 'TX'=>'Texas',
196 | 'UT'=>'Utah',
197 | 'VT'=>'Vermont',
198 | 'VA'=>'Virginia',
199 | 'WA'=>'Washington',
200 | 'WV'=>'West Virginia',
201 | 'WI'=>'Wisconsin',
202 | 'WY'=>'Wyoming',
203 | ];
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartController.php:
--------------------------------------------------------------------------------
1 | success(new CartResource($cart->load(['cartItems.product', 'order'])));
20 | }
21 |
22 | /***************************************************************************************
23 | ** POST
24 | ***************************************************************************************/
25 | public function create(CartRequest $request)
26 | {
27 | $customerForeignKey = Customer::getForeignKey();
28 | $productForeignKey = Product::getForeignKey();
29 |
30 | $cart = Cart::makeOne([
31 | $customerForeignKey => auth()->user()->id ?? null,
32 | 'ip_address' => $request->ip(),
33 | $productForeignKey => $request->get($productForeignKey)
34 | ]);
35 |
36 | return $this->success(new CartResource($cart->fresh()->load(['cartItems.product', 'order'])));
37 | }
38 |
39 | /***************************************************************************************
40 | ** PUT
41 | ***************************************************************************************/
42 | public function update(Cart $cart, CartRequest $request)
43 | {
44 | $cart->updateMe($request->validated());
45 |
46 | return $this->success(new CartResource($cart->load(['cartItems.product', 'order'])));
47 | }
48 |
49 | /***************************************************************************************
50 | ** DELETE
51 | ***************************************************************************************/
52 | public function delete(Cart $cart)
53 | {
54 | $cart->delete();
55 |
56 | return $this->success();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartCouponController.php:
--------------------------------------------------------------------------------
1 | addCoupon($request->validated());
17 |
18 | return $this->success(new CartResource($cart->load(['cartItems.product', 'order'])));
19 | }
20 |
21 | public function delete(Cart $cart)
22 | {
23 | $cart->removeCoupon();
24 |
25 | return $this->success(new CartResource($cart->load(['cartItems.product', 'order'])));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartEmailController.php:
--------------------------------------------------------------------------------
1 | updateEmail($request->validated());
14 |
15 | return $this->success(new CartResource($cart->load('cartItems.product')));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartItemController.php:
--------------------------------------------------------------------------------
1 | validated());
20 |
21 | $cart = $cart->fresh();
22 | $cart->load('cartItems.product');
23 |
24 | return $this->success(new CartResource($cart));
25 | }
26 |
27 | /***************************************************************************************
28 | ** PUT
29 | ***************************************************************************************/
30 | public function update(CartItem $cartItem, CartItemRequest $request)
31 | {
32 | $cartItem->updateMe($request->validated());
33 | $cart = $cartItem->cart;
34 | $cart->load('cartItems.product');
35 |
36 | return $this->success(new CartResource($cart));
37 | }
38 |
39 | /***************************************************************************************
40 | ** DELETE
41 | ***************************************************************************************/
42 | public function delete(CartItem $cartItem)
43 | {
44 | $cartItem->delete();
45 |
46 | return $this->success();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartOptionsController.php:
--------------------------------------------------------------------------------
1 | addOptions($request->validated()['options']);
14 |
15 | return $this->success(new CartResource($cart->load('cartItems.product')));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartShippingController.php:
--------------------------------------------------------------------------------
1 | updateShipping($request->validated());
14 |
15 | return $this->success(new CartResource($cart->load('cartItems.product')));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CartZipCodeController.php:
--------------------------------------------------------------------------------
1 | updateZipCode($request->validated());
14 |
15 | return $this->success(new CartResource($cart->load('cartItems.product')));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Http/Controllers/CheckoutSettingsController.php:
--------------------------------------------------------------------------------
1 | success([
13 | 'required' => CheckoutFields::required(),
14 | 'states' => $state->all(),
15 | 'toc_url' => config('checkout.toc_url'),
16 | 'currency_symbol' => config('checkout.currency.symbol')
17 | ]);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | errors()->all();
30 |
31 | // get first error for message
32 | $collection = collect($errors);
33 | $first_error = $collection->first();
34 | return Responder::noJsonError($errors, $first_error);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Http/Controllers/OrderController.php:
--------------------------------------------------------------------------------
1 | load(['orderItems.product', 'orderPurchase']);
26 |
27 | return $this->success(new OrderResource($order));
28 | }
29 |
30 | /***************************************************************************************
31 | ** POST
32 | ***************************************************************************************/
33 | public function create(OrderRequest $request)
34 | {
35 | $customer = $this->getCustomer($request->order);
36 |
37 | $cart = Cart::byToken($request->order['cart_token'])->firstOrFail();
38 |
39 | try {
40 | $purchase = $this->createPurchase($request, $cart, $customer);
41 | } catch (\Exception $e) {
42 | throw ValidationException::withMessages([
43 | 'payment' => $e->getMessage()
44 | ]);
45 | }
46 |
47 | event(new NewOrderPurchase($purchase));
48 |
49 | $order = \R64\Checkout\Facades\Order::getClassName()::makeOne($purchase, $request->order);
50 | $order->load(['orderItems.product', 'orderPurchase']);
51 |
52 | event(new NewOrder($order));
53 |
54 | return $this->success(new OrderResource($order));
55 | }
56 |
57 | private function getCustomer($order)
58 | {
59 | $customer = auth()->user();
60 |
61 | if (is_null($customer)) {
62 | $customer = new GuestCustomer(
63 | $order['customer_email'],
64 | $order['billing_first_name'],
65 | $order['billing_last_name']
66 | );
67 | }
68 |
69 | return $customer;
70 | }
71 |
72 | private function pay(OrderRequest $request, $customer)
73 | {
74 | $paymentHandler = PaymentHandlerFactory::createFromRequest($request);
75 |
76 | if ($request->isStripe()) {
77 | return $paymentHandler->purchase($request->order, $request->stripe, $customer);
78 | } else if ($request->isPaypal()) {
79 | return $paymentHandler->purchase($request->order, $request->paypal, $customer);
80 | }
81 |
82 | throw new PaymentException("Missing stripe or paypal request information");
83 | }
84 |
85 | /**
86 | * @param OrderRequest $request
87 | * @param $cart
88 | * @param $customer
89 | * @return OrderPurchase
90 | */
91 | protected function createPurchase(OrderRequest $request, $cart, $customer): OrderPurchase
92 | {
93 | if ($cart->total === 0) {
94 | return OrderPurchase::makeFreePurchase($request->order, $customer);
95 | }
96 |
97 | return $this->pay($request, $customer);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartCouponRequest.php:
--------------------------------------------------------------------------------
1 | [
29 | 'string',
30 | Rule::exists('coupons', 'code')->where(function ($query) {
31 | $query->where('active', true);
32 | })
33 | ],
34 | ];
35 | }
36 |
37 | /***************************************************************************************
38 | ** Overriding
39 | ***************************************************************************************/
40 |
41 | /**
42 | * Append "is_update" to Request Input before validation
43 | */
44 | public function addRequestChecks()
45 | {
46 | $data = $this->all();
47 | $data['is_post'] = $this->isPost();
48 | $data['is_update'] = $this->isPut();
49 | $data['is_editing'] = $this->isPost() || $this->isPut();
50 | $data['is_delete'] = $this->isDelete();
51 |
52 | $this->replace($data);
53 |
54 | return $this->all();
55 | }
56 |
57 | /**
58 | * Modify Input Data Before Validation
59 | */
60 | public function validateResolved()
61 | {
62 | $this->addRequestChecks();
63 | parent::validateResolved();
64 | }
65 |
66 | /**
67 | * Modify Conditions of Validator
68 | */
69 | protected function getValidatorInstance()
70 | {
71 | $validator = parent::getValidatorInstance();
72 | // $validator->sometimes();
73 |
74 | $validator->after(function ($validator) {
75 | $this->request->remove('is_post');
76 | $this->request->remove('is_update');
77 | $this->request->remove('is_editing');
78 | $this->request->remove('is_delete');
79 | });
80 |
81 | return $validator;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartEmailRequest.php:
--------------------------------------------------------------------------------
1 | 'email'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartItemRequest.php:
--------------------------------------------------------------------------------
1 | "required_if:is_post,true|integer|exists:${productTableName},id",
31 | 'quantity' => 'integer|gte:0',
32 | 'customer_note' => 'nullable|string'
33 | ];
34 | }
35 |
36 | /***************************************************************************************
37 | ** Overriding
38 | ***************************************************************************************/
39 |
40 | /**
41 | * Append "is_update" to Request Input before validation
42 | */
43 | public function addRequestChecks()
44 | {
45 | $data = $this->all();
46 | $data['is_post'] = $this->isPost();
47 | $data['is_update'] = $this->isPut();
48 | $data['is_editing'] = $this->isPost() || $this->isPut();
49 | $data['is_delete'] = $this->isDelete();
50 |
51 | $this->replace($data);
52 |
53 | return $this->all();
54 | }
55 |
56 | /**
57 | * Modify Input Data Before Validation
58 | */
59 | public function validateResolved()
60 | {
61 | $this->addRequestChecks();
62 | parent::validateResolved();
63 | }
64 |
65 | /**
66 | * Modify Conditions of Validator
67 | */
68 | protected function getValidatorInstance()
69 | {
70 | $validator = parent::getValidatorInstance();
71 | // $validator->sometimes();
72 |
73 | $validator->after(function ($validator) {
74 | $this->request->remove('is_post');
75 | $this->request->remove('is_update');
76 | $this->request->remove('is_editing');
77 | $this->request->remove('is_delete');
78 | });
79 |
80 | return $validator;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartOptionsRequest.php:
--------------------------------------------------------------------------------
1 | 'array'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartRequest.php:
--------------------------------------------------------------------------------
1 | 'nullable|string|email',
29 | 'customer_notes' => 'nullable|string',
30 | 'shipping_first_name' => 'nullable|string',
31 | 'shipping_last_name' => 'nullable|string',
32 | 'shipping_address_line1' => 'nullable|string',
33 | 'shipping_address_line2' => 'nullable|string',
34 | 'shipping_address_city' => 'nullable|string',
35 | 'shipping_address_region' => 'nullable|string',
36 | 'shipping_address_zipcode' => 'nullable|string',
37 | 'shipping_address_phone' => 'nullable|string',
38 | 'billing_same' => 'boolean',
39 | 'billing_first_name' => 'nullable|string',
40 | 'billing_last_name' => 'nullable|string',
41 | 'billing_address_line1' => 'nullable|string',
42 | 'billing_address_line2' => 'nullable|string',
43 | 'billing_address_city' => 'nullable|string',
44 | 'billing_address_region' => 'nullable|string',
45 | 'billing_address_zipcode' => 'nullable|string',
46 | 'billing_address_phone' => 'nullable|string'
47 | ];
48 | }
49 |
50 | /***************************************************************************************
51 | ** Overriding
52 | ***************************************************************************************/
53 |
54 | /**
55 | * Append "is_update" to Request Input before validation
56 | */
57 | public function addRequestChecks()
58 | {
59 | $data = $this->all();
60 | $data['is_post'] = $this->isPost();
61 | $data['is_update'] = $this->isPut();
62 | $data['is_editing'] = $this->isPost() || $this->isPut();
63 | $data['is_delete'] = $this->isDelete();
64 |
65 | $this->replace($data);
66 |
67 | return $this->all();
68 | }
69 |
70 | /**
71 | * Modify Input Data Before Validation
72 | */
73 | public function validateResolved()
74 | {
75 | $this->addRequestChecks();
76 | parent::validateResolved();
77 | }
78 |
79 | /**
80 | * Modify Conditions of Validator
81 | */
82 | protected function getValidatorInstance()
83 | {
84 | $validator = parent::getValidatorInstance();
85 | // $validator->sometimes();
86 |
87 | $validator->after(function ($validator) {
88 | $this->request->remove('is_post');
89 | $this->request->remove('is_update');
90 | $this->request->remove('is_editing');
91 | $this->request->remove('is_delete');
92 | });
93 |
94 | return $validator;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Http/Requests/CartZipCodeRequest.php:
--------------------------------------------------------------------------------
1 | 'postal_code:' . $countryCode
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Http/Requests/JsonFormRequest.php:
--------------------------------------------------------------------------------
1 | method() == 'GET') {
31 | return true;
32 | }
33 | return false;
34 | }
35 |
36 | protected function isPost()
37 | {
38 | if ($this->method() == 'POST') {
39 | return true;
40 | }
41 | return false;
42 | }
43 |
44 | protected function isPut()
45 | {
46 | if ($this->method() == 'PUT') {
47 | return true;
48 | }
49 | return false;
50 | }
51 |
52 | protected function isDelete()
53 | {
54 | if ($this->method() == 'DELETE') {
55 | return true;
56 | }
57 | return false;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Http/Requests/OrderItemRequest.php:
--------------------------------------------------------------------------------
1 | isGet()) {
18 | return true;
19 | }
20 | if ($this->isPost()) {
21 | return auth()->user()->can('create', OrderItem::class);
22 | }
23 | if ($this->isPut()) {
24 | return auth()->user()->can('update', $this->route('orderItem'));
25 | }
26 | if ($this->isDelete()) {
27 | return auth()->user()->can('delete', $this->route('orderItem'));
28 | }
29 |
30 | return true;
31 | }
32 |
33 | /**
34 | * Get the validation rules that apply to the request.
35 | *
36 | * @return array
37 | */
38 | public function rules()
39 | {
40 | $productTableName = Product::getTableName();
41 | $productForeignKey = Product::getForeignKey();
42 |
43 | return [
44 | $productForeignKey => "integer|exists:${productTableName},id",
45 | 'cart_item_id' => 'integer|exists:cart_items,id',
46 | 'price' => 'required_if:is_post,true|integer',
47 | 'quantity' => 'required_if:is_post,true|integer',
48 | 'name' => 'required_if:is_post,true|string|min:2',
49 | ];
50 | }
51 |
52 | /***************************************************************************************
53 | ** Overriding
54 | ***************************************************************************************/
55 |
56 | /**
57 | * Append "is_update" to Request Input before validation
58 | */
59 | public function addRequestChecks()
60 | {
61 | $data = $this->all();
62 | $data['is_post'] = $this->isPost();
63 | $data['is_update'] = $this->isPut();
64 | $data['is_editing'] = $this->isPost() || $this->isPut();
65 | $data['is_delete'] = $this->isDelete();
66 |
67 | $this->replace($data);
68 |
69 | return $this->all();
70 | }
71 |
72 | /**
73 | * Modify Input Data Before Validation
74 | */
75 | public function validateResolved()
76 | {
77 | $this->addRequestChecks();
78 | parent::validateResolved();
79 | }
80 |
81 | /**
82 | * Modify Conditions of Validator
83 | */
84 | protected function getValidatorInstance()
85 | {
86 | $validator = parent::getValidatorInstance();
87 | // $validator->sometimes();
88 |
89 | $validator->after(function ($validator) {
90 | $this->request->remove('is_post');
91 | $this->request->remove('is_update');
92 | $this->request->remove('is_editing');
93 | $this->request->remove('is_delete');
94 | });
95 |
96 | return $validator;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Http/Requests/OrderRequest.php:
--------------------------------------------------------------------------------
1 | isGet()) {
20 | return true;
21 | }
22 |
23 | if ($this->isPut()) {
24 | return false;
25 | }
26 |
27 | if ($this->isDelete()) {
28 | return auth()->user()->can('delete', $this->route('order'));
29 | }
30 |
31 | return true;
32 | }
33 |
34 | /**
35 | * Get the validation rules that apply to the request.
36 | *
37 | * @return array
38 | */
39 | public function rules()
40 | {
41 | $paymentRules = [
42 | 'stripe.token' => [
43 | 'string',
44 | 'max:255',
45 | Rule::requiredIf(function () {
46 | if ($this->has('paypal')) {
47 | return false;
48 | }
49 |
50 | return $this->isCartFree();
51 | })
52 | ],
53 | 'paypal.order_id' => [
54 | 'string',
55 | Rule::requiredIf(function () {
56 | if ($this->has('stripe')) {
57 | return false;
58 | }
59 |
60 | return $this->isCartFree();
61 | })
62 | ],
63 | 'paypal.authorization_id' => [
64 | 'string',
65 | Rule::requiredIf(function () {
66 | if ($this->has('stripe')) {
67 | return false;
68 | }
69 |
70 | return $this->isCartFree();
71 | })
72 | ]
73 | ];
74 |
75 | $orderRules = [
76 | 'cart_token' => 'required_if:is_post,true|string|exists:carts,token',
77 | 'customer_email' => 'required_if:is_post,true|string|email',
78 | 'customer_notes' => 'nullable|string',
79 | 'shipping_first_name' => 'string',
80 | 'shipping_last_name' => 'string',
81 | 'shipping_address_line1' => 'string',
82 | 'shipping_address_line2' => 'string',
83 | 'shipping_address_city' => 'string',
84 | 'shipping_address_region' => 'string',
85 | 'shipping_address_zipcode' => 'string',
86 | 'shipping_address_phone' => 'string',
87 | 'billing_first_name' => 'string',
88 | 'billing_last_name' => 'string',
89 | 'billing_address_line1' => 'string',
90 | 'billing_address_line2' => 'string',
91 | 'billing_address_city' => 'string',
92 | 'billing_address_region' => 'string',
93 | 'billing_address_zipcode' => 'string',
94 | 'billing_address_phone' => 'string'
95 | ];
96 |
97 | $orderRules = $this->addRequiredFields($orderRules);
98 | $orderRules = $this->addOrderPrefix($orderRules);
99 |
100 | return array_merge($paymentRules, $orderRules);
101 | }
102 |
103 | /***************************************************************************************
104 | ** Overriding
105 | ***************************************************************************************/
106 |
107 | /**
108 | * Append "is_update" to Request Input before validation
109 | */
110 | public function addRequestChecks()
111 | {
112 | $data = $this->all();
113 | $data['is_post'] = $this->isPost();
114 | $data['is_update'] = $this->isPut();
115 | $data['is_editing'] = $this->isPost() || $this->isPut();
116 | $data['is_delete'] = $this->isDelete();
117 |
118 | $this->replace($data);
119 |
120 | return $this->all();
121 | }
122 |
123 | /**
124 | * Modify Input Data Before Validation
125 | */
126 | public function validateResolved()
127 | {
128 | $this->addRequestChecks();
129 | parent::validateResolved();
130 | }
131 |
132 | /**
133 | * Modify Conditions of Validator
134 | */
135 | protected function getValidatorInstance()
136 | {
137 | $validator = parent::getValidatorInstance();
138 | // $validator->sometimes();
139 |
140 | $validator->after(function ($validator) {
141 | $this->request->remove('is_post');
142 | $this->request->remove('is_update');
143 | $this->request->remove('is_editing');
144 | $this->request->remove('is_delete');
145 | });
146 |
147 | return $validator;
148 | }
149 |
150 | private function addRequiredFields(array $rules)
151 | {
152 | $requiredFields = CheckoutFields::required();
153 |
154 | return collect($rules)->map(function ($rule, $fieldName) use ($requiredFields) {
155 | if (isset($requiredFields[$fieldName])) {
156 | return $requiredFields[$fieldName] ? 'required_if:is_post,true|' . $rule : 'nullable|' . $rule;
157 | }
158 |
159 | return $rule;
160 | })->toArray();
161 | }
162 |
163 | private function addOrderPrefix(array $rules)
164 | {
165 | return array_combine(
166 | array_map(function ($key) { return "order.${key}"; }, array_keys($rules)),
167 | $rules
168 | );
169 | }
170 |
171 | private function isCartFree()
172 | {
173 | $cartToken = Arr::get($this->get('order'), 'cart_token');
174 |
175 | if (!is_null($cartToken)) {
176 | return false;
177 | }
178 |
179 | $cart = Cart::byToken($cartToken)->firstOrFail();
180 |
181 | return $cart->total > 0;
182 | }
183 |
184 | public function isStripe()
185 | {
186 | return $this->has('stripe');
187 | }
188 |
189 | public function isPaypal()
190 | {
191 | return $this->has('paypal');
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/Http/Resources/CartItemResource.php:
--------------------------------------------------------------------------------
1 | $this->token,
21 | 'price' => displayMoney($this->price),
22 | 'quantity' => $this->quantity,
23 | 'customer_note' => $this->customer_note,
24 | 'product' => new $productResource($this->whenLoaded('product'))
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Http/Resources/CartResource.php:
--------------------------------------------------------------------------------
1 | $this->token,
19 | 'items_subtotal' => displayMoney($this->items_subtotal),
20 | 'tax_rate' => displayTaxRate($this->tax_rate),
21 | 'tax' => displayMoney($this->tax),
22 | 'total' => displayMoney($this->total),
23 | 'total_cents' => $this->total,
24 | 'discount' => displayMoney($this->discount),
25 | 'shipping' => displayMoney($this->shipping),
26 | 'customer_email' => $this->customer_email,
27 | 'shipping_first_name' => $this->shipping_first_name,
28 | 'shipping_last_name' => $this->shipping_last_name,
29 | 'shipping_address_line1' => $this->shipping_address_line1,
30 | 'shipping_address_line2' => $this->shipping_address_line2,
31 | 'shipping_address_city' => $this->shipping_address_city,
32 | 'shipping_address_region' => $this->shipping_address_region,
33 | 'shipping_address_zipcode' => $this->shipping_address_zipcode,
34 | 'shipping_address_phone' => $this->shipping_address_phone,
35 | 'billing_same' => (bool) $this->billing_same,
36 | 'billing_first_name' => $this->billing_first_name,
37 | 'billing_last_name' => $this->billing_last_name,
38 | 'billing_address_line1' => $this->billing_address_line1,
39 | 'billing_address_line2' => $this->billing_address_line2,
40 | 'billing_address_city' => $this->billing_address_city,
41 | 'billing_address_region' => $this->billing_address_region,
42 | 'billing_address_zipcode' => $this->billing_address_zipcode,
43 | 'billing_address_phone' => $this->billing_address_phone,
44 | 'customer_notes' => $this->customer_notes,
45 | 'cart_items' => CartItemResource::collection($this->whenLoaded('cartItems')),
46 | 'order' => new OrderResource($this->whenLoaded('order'))
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Http/Resources/OrderItemResource.php:
--------------------------------------------------------------------------------
1 | $this->name,
21 | 'price' => displayMoney($this->price),
22 | 'quantity' => $this->quantity,
23 | 'customer_note' => $this->customer_note,
24 | 'product' => new $productResource($this->whenLoaded('product'))
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Http/Resources/OrderPurchaseResource.php:
--------------------------------------------------------------------------------
1 | displayMoney($this->amount),
13 | 'card_type' => $this->card_type,
14 | 'card_last4' => $this->card_last4,
15 | 'payment_method' => $this->payment_processor
16 | ];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Http/Resources/OrderResource.php:
--------------------------------------------------------------------------------
1 | $this->token,
19 | 'order_number' => $this->order_number,
20 | 'customer_email' => $this->customer_email,
21 | 'items_total' => displayMoney($this->items_total),
22 | 'shipping' => displayMoney($this->shipping),
23 | 'total' => displayMoney($this->total),
24 | 'tax_rate' => displayTaxRate($this->tax_rate),
25 | 'tax' => displayMoney($this->tax),
26 | 'discount' => displayMoney($this->discount),
27 | 'currency' => $this->currency,
28 | 'shipping_first_name' => $this->shipping_first_name,
29 | 'shipping_last_name' => $this->shipping_last_name,
30 | 'shipping_address_line1' => $this->shipping_address_line1,
31 | 'shipping_address_line2' => $this->shipping_address_line2,
32 | 'shipping_address_city' => $this->shipping_address_city,
33 | 'shipping_address_region' => $this->shipping_address_region,
34 | 'shipping_address_zipcode' => $this->shipping_address_zipcode,
35 | 'shipping_address_phone' => $this->shipping_address_phone,
36 | 'billing_first_name' => $this->billing_first_name,
37 | 'billing_last_name' => $this->billing_last_name,
38 | 'billing_address_line1' => $this->billing_address_line1,
39 | 'billing_address_line2' => $this->billing_address_line2,
40 | 'billing_address_city' => $this->billing_address_city,
41 | 'billing_address_region' => $this->billing_address_region,
42 | 'billing_address_zipcode' => $this->billing_address_zipcode,
43 | 'billing_address_phone' => $this->billing_address_phone,
44 | 'status' => $this->status,
45 | 'customer_notes' => $this->customer_notes,
46 | 'admin_notes' => $this->admin_notes,
47 | 'order_items' => OrderItemResource::collection($this->whenLoaded('orderItems')),
48 | 'order_purchase' => new OrderPurchaseResource($this->whenLoaded('orderPurchase')),
49 | 'created_at' => $this->created_at->format('M, d, Y'),
50 | 'options' => $this->options
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Http/Resources/ProductResource.php:
--------------------------------------------------------------------------------
1 | $this->getName(),
19 | 'image' => $this->getImageUrl(),
20 | 'price' => displayMoney($this->getPrice())
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Models/CartItem.php:
--------------------------------------------------------------------------------
1 | cart->setItemSubtotal();
35 | $cartItem->cart->setDiscount();
36 | $cartItem->cart->setTax();
37 | $cartItem->cart->setTotal();
38 | });
39 | static::updated(function ($cartItem) {
40 | $cartItem->cart->setItemSubtotal();
41 | $cartItem->cart->setDiscount();
42 | $cartItem->cart->setTax();
43 | $cartItem->cart->setTotal();
44 | });
45 | static::deleted(function ($cartItem) {
46 | $cartItem->cart->setItemSubtotal();
47 | $cartItem->cart->setDiscount();
48 | $cartItem->cart->setTax();
49 | $cartItem->cart->setTotal();
50 | });
51 | }
52 |
53 | public function getRouteKeyName()
54 | {
55 | return 'token';
56 | }
57 |
58 | /***************************************************************************************
59 | ** RELATIONS
60 | ***************************************************************************************/
61 |
62 | public function cart()
63 | {
64 | return $this->belongsTo(\R64\Checkout\Facades\Cart::getClassName(), \R64\Checkout\Facades\Cart::getForeignKey());
65 | }
66 |
67 | public function product()
68 | {
69 | return $this->belongsTo(\R64\Checkout\Facades\Product::getClassName(), \R64\Checkout\Facades\Product::getForeignKey());
70 | }
71 |
72 | /***************************************************************************************
73 | ** CRUD
74 | ***************************************************************************************/
75 |
76 | public static function makeOne(Cart $cart, array $data)
77 | {
78 | $productForeignKey = \R64\Checkout\Facades\Product::getForeignKey();
79 | $product = \R64\Checkout\Facades\Product::getClassName()::findOrFail($data[$productForeignKey]);
80 |
81 | $cartItem = $cart->cartItems()->where($productForeignKey, $product->id)->first();
82 |
83 | if (!is_null($cartItem)) {
84 | $data['quantity'] = Arr::get($data, 'quantity') + $cartItem->quantity;
85 | $cartItem->updateMe($data);
86 | return $cartItem;
87 | }
88 |
89 | $cartItem = new CartItem;
90 | $cartItem->{\R64\Checkout\Facades\Cart::getForeignKey()} = $cart->id;
91 | $cartItem->{$productForeignKey} = $product->id;
92 | $cartItem->quantity = Arr::get($data, 'quantity', 1);
93 | $cartItem->price = $product->getPrice() * $cartItem->quantity;
94 | $cartItem->token = Token::generate();
95 | $cartItem->save();
96 |
97 | return $cartItem;
98 | }
99 |
100 | public function updateMe(array $data)
101 | {
102 | $newQuantity = !empty($data['quantity']) ? $data['quantity'] : $this->quantity;
103 | $this->price = $this->product->getPrice() * $newQuantity;
104 | $this->customer_note = Arr::has($data, 'customer_note') ? $data['customer_note'] : $this->customer_note;
105 | $this->quantity = $newQuantity;
106 | $this->save();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Models/Coupon.php:
--------------------------------------------------------------------------------
1 | where('code', $code);
28 | }
29 |
30 | /***************************************************************************************
31 | ** HELPERS
32 | ***************************************************************************************/
33 |
34 | public function calculateDiscount(Cart $cart)
35 | {
36 | if ($this->percentage) {
37 | return $cart->items_subtotal * $this->discount / 100;
38 | }
39 |
40 | $discount = $this->discount;
41 |
42 | return $discount < $cart->items_subtotal ? $discount : $cart->items_subtotal;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Models/Customer.php:
--------------------------------------------------------------------------------
1 | 'boolean',
20 | 'password_migrated' => 'boolean',
21 | ];
22 | public $timestamps = true;
23 |
24 | use SoftDeletes;
25 | use Notifiable;
26 | use HasFactory;
27 |
28 | public function getId()
29 | {
30 | return $this->id;
31 | }
32 |
33 | public function getFirstName()
34 | {
35 | return $this->first_name;
36 | }
37 |
38 | public function getLastName()
39 | {
40 | return $this->last_name;
41 | }
42 |
43 | public function getEmail()
44 | {
45 | return $this->email;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Models/Order.php:
--------------------------------------------------------------------------------
1 | 'json',
24 | 'options' => 'json'
25 | ];
26 | public $timestamps = true;
27 |
28 | /***************************************************************************************
29 | ** MODS
30 | ***************************************************************************************/
31 |
32 | public static function boot()
33 | {
34 | parent::boot();
35 | static::creating(function ($order) {
36 | $order->token = Token::generate();
37 | });
38 |
39 | static::created(function ($order) {
40 | $order->order_number = $order->generateOrderNumber();
41 | $order->save();
42 | });
43 | }
44 |
45 | public function getRouteKeyName()
46 | {
47 | return 'token';
48 | }
49 |
50 | /***************************************************************************************
51 | ** RELATIONS
52 | ***************************************************************************************/
53 |
54 | public function cart()
55 | {
56 | return $this->belongsTo(\R64\Checkout\Facades\Cart::getClassName(), \R64\Checkout\Facades\Cart::getForeignKey());
57 | }
58 |
59 | public function orderItems()
60 | {
61 | return $this->hasMany(\R64\Checkout\Facades\OrderItem::getClassName(), \R64\Checkout\Facades\Order::getForeignKey());
62 | }
63 |
64 | public function customer()
65 | {
66 | return $this->belongsTo(\R64\Checkout\Facades\Customer::getClassName(), \R64\Checkout\Facades\Customer::getForeignKey());
67 | }
68 |
69 | public function orderPurchase()
70 | {
71 | return $this->hasOne(OrderPurchase::class);
72 | }
73 |
74 | /***************************************************************************************
75 | ** CRUD
76 | ***************************************************************************************/
77 |
78 | public static function makeOne(OrderPurchase $purchase, array $data)
79 | {
80 | $order = new self;
81 |
82 | $cart = \R64\Checkout\Facades\Cart::getClassName()::byToken(Arr::get($data, 'cart_token'))->first();
83 |
84 | $customerForeignKey = \R64\Checkout\Facades\Customer::getForeignKey();
85 |
86 | $order->{$customerForeignKey} = $purchase->{$customerForeignKey};
87 | $order->{\R64\Checkout\Facades\Coupon::getForeignKey()} = $cart->{\R64\Checkout\Facades\Coupon::getForeignKey()};
88 | $order->customer_email = !empty($data['customer_email']) ? $data['customer_email'] : $purchase->email;
89 | $order->shipping_first_name = Arr::get($data, 'shipping_first_name');
90 | $order->shipping_last_name = Arr::get($data, 'shipping_last_name');
91 | $order->shipping_address_line1 = Arr::get($data, 'shipping_address_line1');
92 | $order->shipping_address_line2 = Arr::get($data, 'shipping_address_line2');
93 | $order->shipping_address_city = Arr::get($data, 'shipping_address_city');
94 | $order->shipping_address_city = Arr::get($data, 'shipping_address_city');
95 | $order->shipping_address_region = Arr::get($data, 'shipping_address_region');
96 | $order->shipping_address_zipcode = Arr::get($data, 'shipping_address_zipcode');
97 | $order->shipping_address_phone = Arr::get($data, 'shipping_address_phone');
98 |
99 | $order->billing_first_name = Arr::get($data, 'billing_first_name');
100 | $order->billing_last_name = Arr::get($data, 'billing_last_name');
101 | $order->billing_address_line1 = Arr::get($data, 'billing_address_line1');
102 | $order->billing_address_line2 = Arr::get($data, 'billing_address_line2');
103 | $order->billing_address_city = Arr::get($data, 'billing_address_city');
104 | $order->billing_address_region = Arr::get($data, 'billing_address_region');
105 | $order->billing_address_zipcode = Arr::get($data, 'billing_address_zipcode');
106 | $order->billing_address_phone = Arr::get($data, 'billing_address_phone');
107 | $order->status = Arr::get($data, 'status');
108 | $order->customer_notes = Arr::get($data, 'customer_notes');
109 | $order->admin_notes = Arr::get($data, 'admin_notes');
110 | $order->currency = config('checkout.currency.code');
111 | $order->{\R64\Checkout\Facades\Cart::getForeignKey()} = $cart->id;
112 | $order->items_total = $cart->items_subtotal;
113 | $order->tax = $cart->tax;
114 | $order->tax_rate = $cart->tax_rate;
115 | $order->discount = $cart->discount;
116 | $order->shipping = $cart->shipping;
117 | $order->total = $cart->total;
118 | $order->options = $cart->options;
119 | $order->save();
120 |
121 | $cart->cartItems->each(function (CartItem $cartItem) use ($order) {
122 | OrderItem::makeOneFromCartItem($cartItem, $order->id);
123 | });
124 |
125 | $purchase->order()->associate($order);
126 | $purchase->save();
127 |
128 | return $order;
129 | }
130 |
131 | /***************************************************************************************
132 | ** SCOPES
133 | ***************************************************************************************/
134 |
135 | public function scopeByEmail($query, string $email)
136 | {
137 | return $query->where('customer_email', $email);
138 | }
139 |
140 | /***************************************************************************************
141 | ** HELPERS
142 | ***************************************************************************************/
143 |
144 | public function generateOrderNumber()
145 | {
146 | return $this->id;
147 | }
148 |
149 | public function hasDiscount()
150 | {
151 | return $this->discount > 0;
152 | }
153 |
154 | public function hasTax()
155 | {
156 | return $this->tax > 0;
157 | }
158 |
159 | public function hasShipping()
160 | {
161 | return $this->shipping > 0;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Models/OrderItem.php:
--------------------------------------------------------------------------------
1 | belongsTo(\R64\Checkout\Facades\Product::getClassName(), \R64\Checkout\Facades\Product::getForeignKey());
31 | }
32 |
33 | public function cartItem()
34 | {
35 | return $this->belongsTo(\R64\Checkout\Facades\CartItem::getClassName(), \R64\Checkout\Facades\CartItem::getForeignKey());
36 | }
37 |
38 | /***************************************************************************************
39 | ** CRUD
40 | ***************************************************************************************/
41 |
42 | public static function makeOne(array $data)
43 | {
44 | $orderItem = new self;
45 |
46 | $productForeignKey = \R64\Checkout\Facades\Product::getForeignKey();
47 | $cartItemForeignKey = \R64\Checkout\Facades\CartItem::getForeignKey();
48 |
49 | $orderItem->order_id = Arr::get($data, 'order_id');
50 | $orderItem->{$productForeignKey} = Arr::get($data, $productForeignKey);
51 | $orderItem->{$cartItemForeignKey} = Arr::get($data, $cartItemForeignKey);
52 | $orderItem->price = Arr::get($data, 'price');
53 | $orderItem->quantity = Arr::get($data, 'quantity');
54 | $orderItem->name = Arr::get($data, 'name');
55 | $orderItem->customer_note = Arr::get($data, 'customer_note');
56 | $orderItem->save();
57 |
58 | return $orderItem;
59 | }
60 |
61 | public static function makeOneFromCartItem(CartItem $cartItem, $orderId)
62 | {
63 | $product = $cartItem->product;
64 |
65 | $productForeignKey = \R64\Checkout\Facades\Product::getForeignKey();
66 | $orderForeignKey = \R64\Checkout\Facades\Order::getForeignKey();
67 | $cartItemForeignKey = \R64\Checkout\Facades\CartItem::getForeignKey();
68 |
69 | $orderItem = static::makeOne([
70 | $orderForeignKey => $orderId,
71 | $productForeignKey => $product->id,
72 | $cartItemForeignKey => $cartItem->id,
73 | 'price' => $cartItem->price,
74 | 'quantity' => $cartItem->quantity,
75 | 'name' => $product->getName(),
76 | 'customer_note' => $cartItem->customer_note
77 | ]);
78 |
79 | return $orderItem;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Models/OrderPurchase.php:
--------------------------------------------------------------------------------
1 | 'array'
21 | ];
22 | public $timestamps = true;
23 |
24 | /***************************************************************************************
25 | ** RELATIONS
26 | ***************************************************************************************/
27 |
28 | public function order()
29 | {
30 | return $this->belongsTo(\R64\Checkout\Facades\Order::getClassName(), \R64\Checkout\Facades\Order::getForeignKey());
31 | }
32 |
33 | /***************************************************************************************
34 | ** CREATE / UPDATE
35 | ***************************************************************************************/
36 |
37 | public static function makeOne(array $data, CustomerContract $customer)
38 | {
39 | $customerForeignKey = \R64\Checkout\Facades\Customer::getForeignKey();
40 |
41 | $purchase = new self;
42 | $purchase->{$customerForeignKey} = $customer->getId();
43 | $purchase->order_data = $data['order_data'];
44 | $purchase->email = $data['email'];
45 | $purchase->amount = $data['amount'];
46 | $purchase->payment_processor = $data['payment_processor'];
47 |
48 | if ($data['payment_processor'] === PaymentHandlerFactory::STRIPE) {
49 | $purchase->card_type = $data['card_type'];
50 | $purchase->card_last4 = $data['card_last4'];
51 | $purchase->stripe_customer_id = $data['stripe_customer_id'];
52 | $purchase->stripe_card_id = $data['stripe_card_id'];
53 | $purchase->stripe_charge_id = $data['stripe_charge_id'];
54 | $purchase->stripe_fee = round($purchase->amount * config('checkout.stripe.percentage_fee')) + config('checkout.stripe.fixed_fee');
55 | } elseif ($data['payment_processor'] === PaymentHandlerFactory::PAYPAL) {
56 | $purchase->paypal_order_id = $data['paypal_order_id'];
57 | $purchase->paypal_authorization_id = $data['paypal_authorization_id'];
58 | $purchase->paypal_capture_id = $data['paypal_capture_id'];
59 | $purchase->paypal_payer_id = $data['paypal_payer_id'];
60 | $purchase->paypal_fee = $data['paypal_fee'];
61 | }
62 |
63 | $purchase->save();
64 |
65 | return $purchase;
66 | }
67 |
68 | public static function makeFreePurchase(array $data, CustomerContract $customer)
69 | {
70 | $customerForeignKey = \R64\Checkout\Facades\Customer::getForeignKey();
71 |
72 | $purchase = new self;
73 | $purchase->{$customerForeignKey} = $customer->getId();
74 | $purchase->order_data = $data;
75 | $purchase->email = !empty($data['email']) ? $data['email'] : $customer->getEmail();
76 | $purchase->amount = 0;
77 | $purchase->save();
78 |
79 | return $purchase;
80 | }
81 |
82 | /***************************************************************************************
83 | ** SCOPES
84 | ***************************************************************************************/
85 | public function scopeStripe($query)
86 | {
87 | return $query->where('payment_processor', PaymentHandlerFactory::STRIPE);
88 | }
89 |
90 | public function scopeByEmail($query, $email)
91 | {
92 | return $query->where('email', $email);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Models/Product.php:
--------------------------------------------------------------------------------
1 | price;
23 | }
24 |
25 | public function getName()
26 | {
27 | return $this->name;
28 | }
29 |
30 | public function hasImage()
31 | {
32 | }
33 |
34 | public function getImageUrl()
35 | {
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/PaymentException.php:
--------------------------------------------------------------------------------
1 | has(static::STRIPE)) {
22 | return app(StripePaymentHandler::class);
23 | } else if ($request->has(static::PAYPAL)) {
24 | return app(PaypalPaymentHandler::class);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/PaypalPaymentHandler.php:
--------------------------------------------------------------------------------
1 | client = $client;
25 | }
26 |
27 | public function purchase(array $order, array $paymentDetails, Customer $customer)
28 | {
29 | $orderResponse = $this->getPaypalOrder($paymentDetails);
30 |
31 | if ($orderResponse->statusCode !== 200) {
32 | throw new PaymentException("Paypal Status Code {$orderResponse->statusCode}");
33 | }
34 |
35 | $total = $this->getAmount($order);
36 |
37 | if ($this->getPaypalTotal($orderResponse) !== $total) {
38 | throw new PaymentException("Paypal Amount Exception");
39 | }
40 |
41 | $captureResponse = $this->capture($paymentDetails);
42 |
43 | if ($captureResponse->statusCode !== 201) {
44 | throw new PaymentException("Paypal Capture failed");
45 | }
46 |
47 | $captureResponse = $this->captureDetails($this->getPaypalCaptureId($captureResponse));
48 |
49 | return $this->recordPurchase($paymentDetails, $order, $orderResponse, $captureResponse, $customer);
50 | }
51 |
52 | protected function getAmount($order)
53 | {
54 | $cart = \R64\Checkout\Facades\Cart::getClassName()::byToken($order['cart_token'])->first();
55 |
56 | return intval($cart->total);
57 | }
58 |
59 | /**
60 | * @param \PayPalHttp\HttpResponse $response
61 | * @return int
62 | */
63 | protected function getPaypalTotal(\PayPalHttp\HttpResponse $response): int
64 | {
65 | return intval(floatval($response->result->purchase_units[0]->amount->value) * 100);
66 | }
67 |
68 | /**
69 | * @param array $paymentDetails
70 | * @return \PayPalHttp\HttpResponse
71 | */
72 | protected function getPaypalOrder(array $paymentDetails): \PayPalHttp\HttpResponse
73 | {
74 | $response = $this->client->execute(new OrdersGetRequest($paymentDetails['order_id']));
75 |
76 | return $response;
77 | }
78 |
79 | /**
80 | * @param \PayPalHttp\HttpResponse $response
81 | * @return string
82 | */
83 | protected function getPaypalPayerEmail(\PayPalHttp\HttpResponse $response): string
84 | {
85 | return $response->result->payer->email_address;
86 | }
87 |
88 | /**
89 | * @param \PayPalHttp\HttpResponse $response
90 | * @return string
91 | */
92 | protected function getPaypalPayerId(\PayPalHttp\HttpResponse $response): string
93 | {
94 | return $response->result->payer->payer_id;
95 | }
96 |
97 | /**
98 | * @param \PayPalHttp\HttpResponse $response
99 | * @return string
100 | */
101 | protected function getPaypalCaptureId(\PayPalHttp\HttpResponse $response): string
102 | {
103 | return $response->result->id;
104 | }
105 |
106 | /**
107 | * @param \PayPalHttp\HttpResponse $response
108 | * @return string
109 | */
110 | protected function getFee(\PayPalHttp\HttpResponse $response): string
111 | {
112 | return (int) (((float) $response->result->seller_receivable_breakdown->paypal_fee->value) * 100);
113 | }
114 |
115 | /**
116 | * @param array $paymentDetails
117 | *
118 | * @return \PayPalHttp\HttpResponse
119 | */
120 | protected function capture(array $paymentDetails)
121 | {
122 | $request = new AuthorizationsCaptureRequest($paymentDetails['authorization_id']);
123 | $request->body = "{}";
124 | return $this->client->execute($request);
125 | }
126 |
127 | protected function captureDetails($captureId)
128 | {
129 | $request = new CapturesGetRequest($captureId);
130 | return $this->client->execute($request);
131 | }
132 |
133 | protected function recordPurchase(array $paymentDetails, array $order, $orderResponse, $captureResponse, CustomerContract $customer)
134 | {
135 | $customerId = \R64\Checkout\Facades\Customer::getForeignKey();
136 |
137 | return OrderPurchase::makeOne([
138 | $customerId => $customer->getId(),
139 | 'payment_processor' => PaymentHandlerFactory::PAYPAL,
140 | 'order_data' => $order,
141 | 'email' => $this->getPaypalPayerEmail($orderResponse),
142 | 'amount' => $this->getPaypalTotal($orderResponse),
143 | 'paypal_order_id' => $paymentDetails['order_id'],
144 | 'paypal_authorization_id' => $paymentDetails['authorization_id'],
145 | 'paypal_capture_id' => $this->getPaypalCaptureId($captureResponse),
146 | 'paypal_payer_id' => $this->getPaypalPayerId($orderResponse),
147 | 'paypal_fee' => $this->getFee($captureResponse)
148 | ], $customer);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/State.php:
--------------------------------------------------------------------------------
1 | 1,
15 | 'value' => 'Alabama',
16 | 'code' => 'AL',
17 | ],
18 | [
19 | 'id' => 2,
20 | 'value' => 'Alaska',
21 | 'code' => 'AK',
22 | ],
23 | [
24 | 'id' => 3,
25 | 'value' => 'American Samoa',
26 | 'code' => 'AS',
27 | ],
28 | [
29 | 'id' => 4,
30 | 'value' => 'Arizona',
31 | 'code' => 'AZ',
32 | ],
33 | [
34 | 'id' => 5,
35 | 'value' => 'Arkansas',
36 | 'code' => 'AR',
37 | ],
38 | [
39 | 'id' => 6,
40 | 'value' => 'California',
41 | 'code' => 'CA',
42 | ],
43 | [
44 | 'id' => 7,
45 | 'value' => 'Colorado',
46 | 'code' => 'CO',
47 | ],
48 | [
49 | 'id' => 8,
50 | 'value' => 'Connecticut',
51 | 'code' => 'CT',
52 | ],
53 | [
54 | 'id' => 9,
55 | 'value' => 'Delaware',
56 | 'code' => 'DE',
57 | ],
58 | [
59 | 'id' => 10,
60 | 'value' => 'District Of Columbia',
61 | 'code' => 'DC',
62 | ],
63 | [
64 | 'id' => 11,
65 | 'value' => 'Federated States Of Micronesia',
66 | 'code' => 'FM',
67 | ],
68 | [
69 | 'id' => 12,
70 | 'value' => 'Florida',
71 | 'code' => 'FL',
72 | ],
73 | [
74 | 'id' => 13,
75 | 'value' => 'Georgia',
76 | 'code' => 'GA',
77 | ],
78 | [
79 | 'id' => 14,
80 | 'value' => 'Guam',
81 | 'code' => 'GU',
82 | ],
83 | [
84 | 'id' => 15,
85 | 'value' => 'Hawaii',
86 | 'code' => 'HI',
87 | ],
88 | [
89 | 'id' => 16,
90 | 'value' => 'Idaho',
91 | 'code' => 'ID',
92 | ],
93 | [
94 | 'id' => 17,
95 | 'value' => 'Illinois',
96 | 'code' => 'IL',
97 | ],
98 | [
99 | 'id' => 18,
100 | 'value' => 'Indiana',
101 | 'code' => 'IN',
102 | ],
103 | [
104 | 'id' => 19,
105 | 'value' => 'Iowa',
106 | 'code' => 'IA',
107 | ],
108 | [
109 | 'id' => 20,
110 | 'value' => 'Kansas',
111 | 'code' => 'KS',
112 | ],
113 | [
114 | 'id' => 21,
115 | 'value' => 'Kentucky',
116 | 'code' => 'KY',
117 | ],
118 | [
119 | 'id' => 22,
120 | 'value' => 'Louisiana',
121 | 'code' => 'LA',
122 | ],
123 | [
124 | 'id' => 23,
125 | 'value' => 'Maine',
126 | 'code' => 'ME',
127 | ],
128 | [
129 | 'id' => 24,
130 | 'value' => 'Marshall Islands',
131 | 'code' => 'MH',
132 | ],
133 | [
134 | 'id' => 25,
135 | 'value' => 'Maryland',
136 | 'code' => 'MD',
137 | ],
138 | [
139 | 'id' => 26,
140 | 'value' => 'Massachusetts',
141 | 'code' => 'MA',
142 | ],
143 | [
144 | 'id' => 27,
145 | 'value' => 'Michigan',
146 | 'code' => 'MI',
147 | ],
148 | [
149 | 'id' => 28,
150 | 'value' => 'Minnesota',
151 | 'code' => 'MN',
152 | ],
153 | [
154 | 'id' => 29,
155 | 'value' => 'Mississippi',
156 | 'code' => 'MS',
157 | ],
158 | [
159 | 'id' => 30,
160 | 'value' => 'Missouri',
161 | 'code' => 'MO',
162 | ],
163 | [
164 | 'id' => 31,
165 | 'value' => 'Montana',
166 | 'code' => 'MT',
167 | ],
168 | [
169 | 'id' => 32,
170 | 'value' => 'Nebraska',
171 | 'code' => 'NE',
172 | ],
173 | [
174 | 'id' => 33,
175 | 'value' => 'Nevada',
176 | 'code' => 'NV',
177 | ],
178 | [
179 | 'id' => 34,
180 | 'value' => 'New Hampshire',
181 | 'code' => 'NH',
182 | ],
183 | [
184 | 'id' => 35,
185 | 'value' => 'New Jersey',
186 | 'code' => 'NJ',
187 | ],
188 | [
189 | 'id' => 36,
190 | 'value' => 'New Mexico',
191 | 'code' => 'NM',
192 | ],
193 | [
194 | 'id' => 37,
195 | 'value' => 'New York',
196 | 'code' => 'NY',
197 | ],
198 | [
199 | 'id' => 38,
200 | 'value' => 'North Carolina',
201 | 'code' => 'NC',
202 | ],
203 | [
204 | 'id' => 39,
205 | 'value' => 'North Dakota',
206 | 'code' => 'ND',
207 | ],
208 | [
209 | 'id' => 40,
210 | 'value' => 'Northern Mariana Islands',
211 | 'code' => 'MP',
212 | ],
213 | [
214 | 'id' => 41,
215 | 'value' => 'Ohio',
216 | 'code' => 'OH',
217 | ],
218 | [
219 | 'id' => 42,
220 | 'value' => 'Oklahoma',
221 | 'code' => 'OK',
222 | ],
223 | [
224 | 'id' => 43,
225 | 'value' => 'Oregon',
226 | 'code' => 'OR',
227 | ],
228 | [
229 | 'id' => 44,
230 | 'value' => 'Palau',
231 | 'code' => 'PW',
232 | ],
233 | [
234 | 'id' => 45,
235 | 'value' => 'Pennsylvania',
236 | 'code' => 'PA',
237 | ],
238 | [
239 | 'id' => 46,
240 | 'value' => 'Puerto Rico',
241 | 'code' => 'PR',
242 | ],
243 | [
244 | 'id' => 47,
245 | 'value' => 'Rhode Island',
246 | 'code' => 'RI',
247 | ],
248 | [
249 | 'id' => 48,
250 | 'value' => 'South Carolina',
251 | 'code' => 'SC',
252 | ],
253 | [
254 | 'id' => 49,
255 | 'value' => 'South Dakota',
256 | 'code' => 'SD',
257 | ],
258 | [
259 | 'id' => 50,
260 | 'value' => 'Tennessee',
261 | 'code' => 'TN',
262 | ],
263 | [
264 | 'id' => 51,
265 | 'value' => 'Texas',
266 | 'code' => 'TX',
267 | ],
268 | [
269 | 'id' => 52,
270 | 'value' => 'Utah',
271 | 'code' => 'UT',
272 | ],
273 | [
274 | 'id' => 53,
275 | 'value' => 'Vermont',
276 | 'code' => 'VT',
277 | ],
278 | [
279 | 'id' => 54,
280 | 'value' => 'Virgin Islands',
281 | 'code' => 'VI',
282 | ],
283 | [
284 | 'id' => 55,
285 | 'value' => 'Virginia',
286 | 'code' => 'VA',
287 | ],
288 | [
289 | 'id' => 56,
290 | 'value' => 'Washington',
291 | 'code' => 'WA',
292 | ],
293 | [
294 | 'id' => 57,
295 | 'value' => 'West Virginia',
296 | 'code' => 'WV',
297 | ],
298 | [
299 | 'id' => 58,
300 | 'value' => 'Wisconsin',
301 | 'code' => 'WI',
302 | ],
303 | [
304 | 'id' => 59,
305 | 'value' => 'Wyoming',
306 | 'code' => 'WY',
307 | ],
308 | ];
309 | }
310 |
311 | public function getByCode($stateCode)
312 | {
313 | return collect($this->all())->filter(function ($state) use ($stateCode) {
314 | return $state['code'] === $stateCode;
315 | })->first();
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/src/StripeMockHandler.php:
--------------------------------------------------------------------------------
1 | getMockStripeCharge($params);
20 | $charge = $stripeCharge::create($params, $this->stripeConnectParam());
21 |
22 | m::close();
23 |
24 | if ($charge) {
25 | return new Charge($charge);
26 | }
27 | }
28 |
29 | private function getMockStripeCharge($params)
30 | {
31 | $charge = m::mock('alias:StripeCharge');
32 |
33 | $charge
34 | ->shouldReceive('create')
35 | ->with([
36 | 'customer' => $params['customer'],
37 | 'amount' => $params['amount'],
38 | 'currency' => $params['currency']
39 | ], $this->stripeConnectParam())
40 | ->andReturn($this->getStripeCharge($params));
41 |
42 | $this->successful = true;
43 |
44 | return $charge;
45 | }
46 |
47 | private function getStripeCharge($params)
48 | {
49 | $faker = Factory::create();
50 |
51 | $charge = new StripeCharge(['id' => 'ch_1']);
52 | $charge->amount = $params['amount'];
53 | $charge->currency = $params['currency'];
54 | $charge->created = time();
55 | $charge->source = (object) [
56 | 'id' => 'card_1',
57 | 'object' => 'card',
58 | 'name' => $faker->name,
59 | 'brand' => $faker->creditCardType,
60 | 'last4' => '4242',
61 | 'exp_month' => $faker->numberBetween(1, 12),
62 | 'exp_year' => now()->addYear()->year,
63 | 'country' => 'US',
64 | ];
65 |
66 | return $charge;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/StripePaymentHandler.php:
--------------------------------------------------------------------------------
1 | processor = $processor;
22 | }
23 |
24 | public function purchase(array $order, array $paymentDetails, CustomerContract $customer)
25 | {
26 | if (!isset($paymentDetails['token'])) {
27 | throw new PaymentException("Stripe: Token is missing");
28 | }
29 |
30 | // Create Customer
31 | $stripeCustomer = $this->getOrCreateCustomer($order, $paymentDetails, $customer);
32 |
33 | // Create Transaction
34 | $paymentResponse = $this->makePaymentAttempt($order, $stripeCustomer);
35 |
36 | // Record Purchase
37 | return $this->recordPurchase($paymentResponse, $order, $customer, $stripeCustomer);
38 | }
39 |
40 | protected function getOrCreateCustomer(array $order, array $paymentDetails, CustomerContract $customer)
41 | {
42 | $orderPurchase = OrderPurchase::byEmail($customer->getEmail())->stripe()->first();
43 |
44 | if ($orderPurchase) {
45 | $stripeCustomer = $this->processor->getCustomer($orderPurchase->stripe_customer_id);
46 |
47 | if ($stripeCustomer) {
48 | return $stripeCustomer;
49 | }
50 | }
51 |
52 | $email = $order['customer_email'];
53 | $firstName = $order['billing_first_name'];
54 | $lastName = $order['billing_last_name'];
55 |
56 | $stripeCustomer = $this->processor->createCustomer([
57 | 'description' => '64robots checkout ' . $firstName . ' ' . $lastName,
58 | 'source' => $paymentDetails['token'],
59 | 'email' => $email,
60 | 'metadata' => [
61 | 'first_name' => $firstName,
62 | 'last_name' => $lastName,
63 | 'email' => $email
64 | ],
65 | ]);
66 |
67 | if (!$this->processor->attemptSuccessful()) {
68 | throw new PaymentException("Stripe: " . $this->processor->getErrorMessage());
69 | }
70 | return $stripeCustomer;
71 | }
72 |
73 | protected function makePaymentAttempt(array $order, StripeCustomer $customer)
74 | {
75 | return $this->chargeCard($order, $customer);
76 | }
77 |
78 | protected function chargeCard(array $order, StripeCustomer $customer)
79 | {
80 | $charge = $this->processor->createCharge([
81 | 'customer' => $customer->id,
82 | 'amount' => $this->getAmount($order),
83 | 'currency' => 'USD'
84 | ]);
85 |
86 | if (! $this->processor->attemptSuccessful()) {
87 | throw new PaymentException("Stripe: " . $this->processor->getErrorMessage());
88 | }
89 |
90 | return $charge;
91 | }
92 |
93 | protected function getAmount($order)
94 | {
95 | $cart = \R64\Checkout\Facades\Cart::getClassName()::byToken($order['cart_token'])->first();
96 |
97 | return $cart->total;
98 | }
99 |
100 | protected function recordPurchase($paymentResponse, array $order, CustomerContract $customer, StripeCustomer $stripeCustomer)
101 | {
102 | $customerId = \R64\Checkout\Facades\Customer::getForeignKey();
103 |
104 | return OrderPurchase::makeOne([
105 | $customerId => $customer->getId(),
106 | 'payment_processor' => PaymentHandlerFactory::STRIPE,
107 | 'order_data' => $order,
108 | 'email' => $stripeCustomer->email,
109 | 'amount' => $paymentResponse->amount,
110 | 'card_type' => $paymentResponse->card->brand,
111 | 'card_last4' => $paymentResponse->card->last4,
112 | 'stripe_customer_id' => $stripeCustomer->id,
113 | 'stripe_card_id' => $paymentResponse->card_id,
114 | 'stripe_charge_id' => $paymentResponse->id
115 | ], $customer);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/64robots/checkout/35f1fd16d7990b2ef31f56ac67c83b46a1cc7412/tests/.gitkeep
--------------------------------------------------------------------------------
/tests/Controllers/CartControllerTest.php:
--------------------------------------------------------------------------------
1 | [
23 | '*' => [
24 | 'cart_item_token',
25 | 'price',
26 | 'quantity',
27 | 'customer_note',
28 | 'product' => [
29 | 'name',
30 | 'image',
31 | ],
32 | ],
33 | ],
34 | ];
35 |
36 | /**
37 | * @test
38 | * GET /api/cart/{cart}
39 | */
40 | public function anybody_can_view_cart_by_token()
41 | {
42 | $cart = Cart::factory()->withProducts()->create();
43 |
44 | $this
45 | ->json('GET', "/api/carts/{$cart->token}")
46 | ->assertStatus(200)
47 | ->assertJson(['success' => true])
48 | ->assertJsonStructure([
49 | 'success',
50 | 'data' => $this->cartStructure,
51 | ]);
52 | }
53 |
54 | /**
55 | * @test
56 | * POST /api/carts
57 | */
58 | public function anybody_can_create_an_empty_cart()
59 | {
60 | $response = $this->json('POST', '/api/carts', [])
61 | ->assertStatus(200)
62 | ->assertJson(['success' => true])
63 | ->assertJsonStructure([
64 | 'success',
65 | 'data' => $this->cartStructure,
66 | ]);
67 |
68 | $cart = json_decode($response->getContent(), true)['data'];
69 |
70 | $this->assertCount(0, $cart['cart_items']);
71 | $this->assertDatabaseHas('carts', [
72 | 'token' => $cart['cart_token'],
73 | 'total' => 0,
74 | 'items_subtotal' => 0,
75 | 'tax' => 0,
76 | 'tax_rate' => 0,
77 | ]);
78 | }
79 |
80 | /**
81 | * @test
82 | * POST /api/carts
83 | */
84 | public function anybody_can_create_cart_with_one_item()
85 | {
86 | $product = Product::factory()->create();
87 |
88 | $response = $this->json('POST', '/api/carts', ['product_id' => $product->id]);
89 |
90 | $response->assertStatus(200)
91 | ->assertJson(['success' => true])
92 | ->assertJsonStructure([
93 | 'success',
94 | 'data' => $this->cartStructure,
95 | ]);
96 |
97 | $cart = json_decode($response->getContent(), true)['data'];
98 |
99 | $this->assertCount(1, $cart['cart_items']);
100 | $this->assertDatabaseHas('carts', [
101 | 'token' => $cart['cart_token'],
102 | 'items_subtotal' => $product->getPrice(),
103 | ]);
104 | }
105 |
106 | /**
107 | * @test
108 | * PUT /api/carts/{cart}
109 | */
110 | public function anybody_can_update_a_cart()
111 | {
112 | $cart = Cart::factory()->withProducts()->create();
113 |
114 | $this->json('PUT', "/api/carts/{$cart->token}", [
115 | 'customer_notes' => "here we go, it's a note",
116 | ])
117 | ->assertStatus(200)
118 | ->assertJson(['success' => true])
119 | ->assertJsonStructure([
120 | 'success',
121 | 'data' => $this->cartStructure,
122 | ]);
123 |
124 | $this->assertDatabaseHas('carts', [
125 | 'customer_notes' => "here we go, it's a note",
126 | ]);
127 | }
128 |
129 | /**
130 | * @test
131 | * PUT /api/carts/{cart}
132 | */
133 | public function billing_information_is_the_same_as_shipping_by_default()
134 | {
135 | $cart = Cart::factory()->create();
136 |
137 | $response = $this->json('PUT', "/api/carts/{$cart->token}", [
138 | 'shipping_first_name' => "first name",
139 | 'shipping_last_name' => "last name",
140 | 'shipping_address_line1' => "line 1",
141 | 'shipping_address_line2' => "line 2",
142 | 'shipping_address_city' => "city",
143 | 'shipping_address_region' => "region",
144 | 'shipping_address_zipcode' => "zipcode",
145 | 'shipping_address_phone' => "123123",
146 | ])
147 | ->assertStatus(200)
148 | ->assertJson(['success' => true])
149 | ->assertJsonStructure([
150 | 'success',
151 | 'data' => $this->cartStructure,
152 | ]);
153 |
154 | $response = json_decode($response->getContent(), true)['data'];
155 |
156 | $this->assertEquals($response['shipping_first_name'], $response['billing_first_name']);
157 | $this->assertEquals($response['shipping_last_name'], $response['billing_last_name']);
158 | $this->assertEquals($response['shipping_address_line1'], $response['billing_address_line1']);
159 | $this->assertEquals($response['shipping_address_line2'], $response['billing_address_line2']);
160 | $this->assertEquals($response['shipping_address_city'], $response['billing_address_city']);
161 | $this->assertEquals($response['shipping_address_region'], $response['billing_address_region']);
162 | $this->assertEquals($response['shipping_address_zipcode'], $response['billing_address_zipcode']);
163 | $this->assertEquals($response['shipping_address_phone'], $response['billing_address_phone']);
164 | }
165 |
166 | /**
167 | * @test
168 | * PUT /api/carts/{cart}
169 | */
170 | public function billing_information_is_not_the_same_as_shipping_when_billing_same_is_false()
171 | {
172 | $cart = Cart::factory()->create(['billing_same' => false]);
173 |
174 | $response = $this->json('PUT', "/api/carts/{$cart->token}", [
175 | 'shipping_first_name' => "first name",
176 | 'shipping_last_name' => "last name",
177 | 'shipping_address_line1' => "line 1",
178 | 'shipping_address_line2' => "line 2",
179 | 'shipping_address_city' => "city",
180 | 'shipping_address_region' => "region",
181 | 'shipping_address_zipcode' => "zipcode",
182 | 'shipping_address_phone' => "123123",
183 | 'billing_same' => false
184 | ])
185 | ->assertStatus(200)
186 | ->assertJson(['success' => true])
187 | ->assertJsonStructure([
188 | 'success',
189 | 'data' => $this->cartStructure,
190 | ]);
191 |
192 | $response = json_decode($response->getContent(), true)['data'];
193 |
194 | $this->assertNull($response['billing_first_name']);
195 | $this->assertNull($response['billing_last_name']);
196 | $this->assertNull($response['billing_address_line1']);
197 | $this->assertNull($response['billing_address_line2']);
198 | $this->assertNull($response['billing_address_city']);
199 | $this->assertNull($response['billing_address_region']);
200 | $this->assertNull($response['billing_address_zipcode']);
201 | $this->assertNull($response['billing_address_phone']);
202 | }
203 |
204 | /**
205 | * @test
206 | * DELETE /api/carts
207 | */
208 | public function anybody_can_delete_a_cart()
209 | {
210 | $cart = Cart::factory()->withProducts()->create();
211 |
212 | $this->json('DELETE', "/api/carts/{$cart->token}")
213 | ->assertJson(['success' => true]);
214 |
215 | $this->assertSoftDeleted('carts', [
216 | 'token' => $cart->token,
217 | ]);
218 |
219 | $this->assertSoftDeleted('cart_items', [
220 | 'id' => $cart->cartItems()->withTrashed()->first()->id,
221 | ]);
222 | }
223 |
224 | /**
225 | * @test
226 | * POST /api/carts
227 | */
228 | public function customer_can_create_an_empty_cart()
229 | {
230 | $customer = Customer::factory()->create();
231 |
232 | $response = $this->actingAs($customer)
233 | ->json('POST', '/api/carts', [])
234 | ->assertStatus(200)
235 | ->assertJson(['success' => true])
236 | ->assertJsonStructure([
237 | 'success',
238 | 'data' => $this->cartStructure,
239 | ]);
240 |
241 | $cart = json_decode($response->getContent(), true)['data'];
242 |
243 | $this->assertCount(0, $cart['cart_items']);
244 | $this->assertDatabaseHas('carts', [
245 | 'token' => $cart['cart_token'],
246 | 'customer_id' => $customer->id,
247 | 'total' => 0,
248 | 'items_subtotal' => 0,
249 | ]);
250 | }
251 |
252 | /**
253 | * @test
254 | * POST /api/carts
255 | */
256 | public function customer_can_create_cart_with_one_item()
257 | {
258 | $customer = Customer::factory()->create();
259 | $product = Product::factory()->create();
260 |
261 | $response = $this->actingAs($customer)
262 | ->json('POST', '/api/carts', ['product_id' => $product->id])
263 | ->assertStatus(200)
264 | ->assertJson(['success' => true])
265 | ->assertJsonStructure([
266 | 'success',
267 | 'data' => $this->cartStructure,
268 | ]);
269 |
270 | $cart = json_decode($response->getContent(), true)['data'];
271 |
272 | $this->assertCount(1, $cart['cart_items']);
273 | $this->assertDatabaseHas('carts', [
274 | 'token' => $cart['cart_token'],
275 | 'items_subtotal' => $product->getPrice(),
276 | ]);
277 | }
278 |
279 | /**
280 | * @test
281 | * PUT /api/carts/{cart}
282 | */
283 | public function customer_can_update_a_cart()
284 | {
285 | $customer = Customer::factory()->create([
286 | 'email' => 'email@email.com',
287 | ]);
288 | $cart = Cart::factory()->withProducts()->create([
289 | 'customer_id' => $customer->id,
290 | ]);
291 |
292 | $this->actingAs($customer)
293 | ->json('PUT', "/api/carts/{$cart->token}", [
294 | 'customer_email' => 'new@email.com',
295 | ])
296 | ->assertStatus(200)
297 | ->assertJson(['success' => true])
298 | ->assertJsonStructure([
299 | 'success',
300 | 'data' => $this->cartStructure,
301 | ]);
302 |
303 | $this->assertDatabaseHas('carts', [
304 | 'customer_email' => 'new@email.com',
305 | ]);
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/tests/Controllers/CartCouponControllerTest.php:
--------------------------------------------------------------------------------
1 | [
24 | '*' => [
25 | 'cart_item_token',
26 | 'price',
27 | 'quantity',
28 | 'customer_note',
29 | 'product' => [
30 | 'name',
31 | 'image',
32 | ],
33 | ],
34 | ],
35 | ];
36 |
37 | /**
38 | * @test
39 | * PUT /api/carts/{cart}/coupon-code
40 | */
41 | public function discount_code_discounts_the_total_price()
42 | {
43 | $cart = Cart::factory()->create(['shipping' => 0]);
44 | $product = Product::factory()->create(['price' => 100000]);
45 |
46 | CartItem::makeOne($cart, ['product_id' => $product->id]);
47 |
48 | $coupon = Coupon::factory()->tenDollarsOff()->create();
49 |
50 | $response = $this->json('PUT', "/api/carts/{$cart->token}/coupon-code", [
51 | 'coupon_code' => $coupon->code,
52 | ])
53 | ->assertStatus(200)
54 | ->assertJson(['success' => true])
55 | ->assertJsonStructure([
56 | 'success',
57 | 'data' => $this->cartStructure,
58 | ]);
59 |
60 | $response = json_decode($response->getContent(), true)['data'];
61 |
62 | $this->assertEquals('10.00', $response['discount']);
63 | $this->assertEquals('1,000.00', $response['items_subtotal']);
64 | $this->assertEquals('990.00', $response['total']);
65 | }
66 |
67 | /**
68 | * @test
69 | * DELETE /api/carts/{cart}/coupon-code
70 | */
71 | public function removing_discount_code_removes_discount_from_total()
72 | {
73 | $cart = Cart::factory()->create(['shipping' => 0]);
74 | $product = Product::factory()->create(['price' => 100000]);
75 | CartItem::makeOne($cart, ['product_id' => $product->id]);
76 |
77 | $coupon = Coupon::factory()->tenDollarsOff()->create();
78 |
79 | $this->json('PUT', "/api/carts/{$cart->token}/coupon-code", [
80 | 'coupon_code' => $coupon->code,
81 | ]);
82 |
83 | $this->assertDatabaseHas('carts', [
84 | 'token' => $cart->token,
85 | 'coupon_id' => $coupon->id
86 | ]);
87 |
88 | $response = $this->json('DELETE', "/api/carts/{$cart->token}/coupon-code");
89 |
90 | $response = json_decode($response->getContent(), true)['data'];
91 |
92 | $this->assertEquals('0.00', $response['discount']);
93 | $this->assertEquals('1,000.00', $response['items_subtotal']);
94 | $this->assertEquals('1,000.00', $response['total']);
95 |
96 | $this->assertDatabaseMissing('carts', [
97 | 'token' => $cart->token,
98 | 'coupon_id' => $coupon->id
99 | ]);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/tests/Controllers/CartItemControllerTest.php:
--------------------------------------------------------------------------------
1 | [
22 | '*' => [
23 | 'cart_item_token',
24 | 'price',
25 | 'quantity',
26 | 'customer_note',
27 | 'product' => [
28 | 'name',
29 | 'image',
30 | ],
31 | ],
32 | ],
33 | ];
34 |
35 | private $cartItemStructure = [
36 | 'cart_item_token',
37 | 'price',
38 | 'quantity',
39 | 'customer_note',
40 | 'product' => [
41 | 'name',
42 | 'image',
43 | 'price'
44 | ]
45 | ];
46 |
47 | /**
48 | * @test
49 | * POST /api/carts/{cart}/cart-tiems
50 | */
51 | public function anyone_can_add_a_product_to_a_cart()
52 | {
53 | $cart = Cart::factory()->create();
54 | $product = Product::factory()->create();
55 |
56 | $this->json('POST', "/api/carts/{$cart->token}/cart-items", [
57 | 'product_id' => $product->id,
58 | 'quantity' => 5
59 | ])
60 | ->assertStatus(200)
61 | ->assertJson(['success' => true])
62 | ->assertJsonStructure([
63 | 'success',
64 | 'data' => $this->cartStructure
65 | ]);
66 |
67 |
68 | $this->assertDatabaseHas('cart_items', ['cart_id' => $cart->id, 'product_id' => $product->id]);
69 | }
70 |
71 | /**
72 | * @test
73 | * PUT /api/cart-items/{cartItem}
74 | */
75 | public function anyone_can_update_cart_item_quantity()
76 | {
77 | $cart = Cart::factory()->withProducts()->create();
78 | $cartItem = $cart->cartItems->first();
79 |
80 | $newQuantity = $cartItem->quantity + 10;
81 |
82 | $this->json('PUT', "api/cart-items/{$cartItem->token}", [
83 | 'quantity' => $newQuantity
84 | ])
85 | ->assertStatus(200)
86 | ->assertJson(['success' => true])
87 | ->assertJsonStructure([
88 | 'success',
89 | 'data' => $this->cartStructure
90 | ]);
91 |
92 | $this->assertDatabaseHas('cart_items', ['id' => $cartItem->id, 'quantity' => $newQuantity]);
93 | }
94 |
95 | /**
96 | * @test
97 | * PUT /api/cart-items/{cartItem}
98 | */
99 | public function cart_total_changes_when_new_product_is_added_into_the_cart()
100 | {
101 | $this->withoutExceptionHandling();
102 | $cartResponse = $this->json('POST', "/api/carts")
103 | ->assertStatus(200)
104 | ->assertJson(['success' => true])
105 | ->assertJsonStructure([
106 | 'success',
107 | 'data' => $this->cartStructure
108 | ]);
109 |
110 | $cartResponse = json_decode($cartResponse->getContent(), true)['data'];
111 |
112 | $this->assertEquals('0.00', $cartResponse['items_subtotal']);
113 | $this->assertEquals('0.00', $cartResponse['total']);
114 | $this->assertEmpty($cartResponse['cart_items']);
115 |
116 | $product = Product::factory()->create(['price' => 1000]);
117 |
118 | $cartResponse = $this->json('POST', "/api/carts/${cartResponse['cart_token']}/cart-items", [
119 | 'product_id' => $product->id,
120 | 'quantity' => 2
121 | ])
122 | ->assertStatus(200)
123 | ->assertJson(['success' => true])
124 | ->assertJsonStructure([
125 | 'success',
126 | 'data' => $this->cartStructure
127 | ]);
128 |
129 |
130 | $cartResponse = json_decode($cartResponse->getContent(), true)['data'];
131 |
132 | $this->assertEquals("20.00", $cartResponse['items_subtotal']);
133 | $this->assertEquals("20.00", $cartResponse['total']);
134 | $this->assertCount(1, $cartResponse['cart_items']);
135 | $this->assertEquals("20.00", $cartResponse['cart_items'][0]['price']);
136 | $this->assertEquals("2", $cartResponse['cart_items'][0]['quantity']);
137 | $this->assertEquals("10.00", $cartResponse['cart_items'][0]['product']['price']);
138 | }
139 |
140 | /**
141 | * @test
142 | * DELETE /api/cart-items/{cartItem}
143 | */
144 | public function anyone_can_delete_cart_item()
145 | {
146 | $cart = Cart::factory()->withProducts()->create();
147 | $cartItem = $cart->cartItems->first();
148 |
149 | $this->json('DELETE', "/api/cart-items/{$cartItem->token}")
150 | ->assertJson(['success' => true]);
151 |
152 | $this->assertSoftDeleted('cart_items', [
153 | 'id' => $cartItem->id,
154 | ]);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/tests/Controllers/CartZipCodeControllerTest.php:
--------------------------------------------------------------------------------
1 | instance(GeoNames::class, new FakeGeoNames(new Client(), 'username', 'code'));
24 | $cart = Cart::factory()->create();
25 |
26 | $response = $this->json('PUT', "/api/carts/{$cart->token}/zipcode", [
27 | 'zipcode' => 21224,
28 | ])
29 | ->assertStatus(200)
30 | ->assertJson(['success' => true]);
31 |
32 | $response = json_decode($response->getContent(), true)['data'];
33 |
34 | $this->assertEquals($response['shipping_address_city'], 'Baltimore');
35 | $this->assertEquals($response['shipping_address_region'], 'Maryland');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Controllers/CheckoutSettingsControllerTest.php:
--------------------------------------------------------------------------------
1 | json('GET', '/api/checkout/settings')
17 | ->assertStatus(200)
18 | ->assertJson(['success' => true])
19 | ->assertJsonStructure([
20 | 'success',
21 | 'data' => [
22 | 'required',
23 | 'states',
24 | 'toc_url',
25 | 'currency_symbol'
26 | ]
27 | ]);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Controllers/OrderControllerTest.php:
--------------------------------------------------------------------------------
1 | [
47 | '*' => [
48 | 'name',
49 | 'price',
50 | 'quantity'
51 | ]
52 | ],
53 | 'order_purchase' => [
54 | 'amount',
55 | 'card_type',
56 | 'card_last4'
57 | ],
58 | 'created_at'
59 | ];
60 |
61 | protected function setUp(): void
62 | {
63 | parent::setUp();
64 |
65 | $this->app->bind(StripeInterface::class, function ($app) {
66 | $options['secret_key'] = $app['config']->get('stripe.secret');
67 | $options['stripe_connect_id'] = $app['config']->get('stripe.connect_id') ?? $app->request->get('stripe_connect_id');
68 | $options['skip_stripe_connect'] = $app['config']->get('stripe.skip_connect') ?? $app->request->get('skip_stripe_connect', true);
69 |
70 | return new StripeMockHandler($options);
71 | });
72 | }
73 |
74 | /**
75 | * @test
76 | * POST /api/orders
77 | */
78 | public function anyone_can_create_an_order()
79 | {
80 | $cart = Cart::factory()->withProducts()->create();
81 |
82 | $response = $this->json('POST', '/api/orders', [
83 | 'stripe' => [
84 | 'token' => 'random token'
85 | ],
86 | 'order' => [
87 | 'cart_token' => $cart->token,
88 | 'customer_email' => 'email@gmail.com',
89 | 'shipping_first_name' => 'First name',
90 | 'shipping_last_name' => 'Last name',
91 | 'shipping_address_line1' => 'Street 1',
92 | 'shipping_address_line2' => 'Line 2',
93 | 'shipping_address_city' => 'Beverly Hills',
94 | 'shipping_address_region' => 'California',
95 | 'shipping_address_zipcode' => '90210',
96 | 'shipping_address_phone' => '123321123',
97 | 'billing_first_name' => 'First name',
98 | 'billing_last_name' => 'Last name',
99 | 'billing_address_line1' => 'Street 1',
100 | 'billing_address_line2' => 'Line 2',
101 | 'billing_address_city' => 'Beverly Hills',
102 | 'billing_address_region' => 'California',
103 | 'billing_address_zipcode' => '90210',
104 | 'billing_address_phone' => '123321123',
105 | ]
106 | ])
107 | ->assertStatus(200)
108 | ->assertJson(['success' => true])
109 | ->assertJsonStructure([
110 | 'success',
111 | 'data' => $this->orderStructure
112 | ]);
113 |
114 | $cart = $this->responseToData($this->json('GET', '/api/carts/' . $cart->token));
115 | $order = $this->responseToData($response);
116 |
117 | $this->assertEquals('email@gmail.com', $order['customer_email']);
118 | $this->assertEquals('First name', $order['shipping_first_name']);
119 | $this->assertEquals('Last name', $order['shipping_last_name']);
120 | $this->assertEquals('Street 1', $order['shipping_address_line1']);
121 | $this->assertEquals('Line 2', $order['shipping_address_line2']);
122 | $this->assertEquals('Beverly Hills', $order['shipping_address_city']);
123 | $this->assertEquals('California', $order['shipping_address_region']);
124 | $this->assertEquals('90210', $order['shipping_address_zipcode']);
125 | $this->assertEquals('123321123', $order['shipping_address_phone']);
126 | $this->assertEquals('First name', $order['billing_first_name']);
127 | $this->assertEquals('Last name', $order['billing_last_name']);
128 | $this->assertEquals('Street 1', $order['billing_address_line1']);
129 | $this->assertEquals('Line 2', $order['billing_address_line2']);
130 | $this->assertEquals('Beverly Hills', $order['billing_address_city']);
131 | $this->assertEquals('California', $order['billing_address_region']);
132 | $this->assertEquals('90210', $order['billing_address_zipcode']);
133 | $this->assertEquals('123321123', $order['billing_address_phone']);
134 |
135 | $this->assertEquals(1, $order['order_number']);
136 | $this->assertEquals($cart['items_subtotal'], $order['items_total']);
137 | $this->assertEquals($cart['shipping'], $order['shipping']);
138 | $this->assertEquals($cart['total'], $order['total']);
139 | $this->assertEquals($cart['tax'], $order['tax']);
140 | $this->assertEquals($cart['tax_rate'], $order['tax_rate']);
141 | $this->assertCount(count($cart['cart_items']), $order['order_items']);
142 |
143 | $this->assertEquals($cart['total'], $order['order_purchase']['amount']);
144 | }
145 |
146 | /**
147 | * @test
148 | * GET /api/orders/{order-token}
149 | */
150 | public function anyone_can_get_an_order_by_token()
151 | {
152 | $cart = Cart::factory()->withProducts()->create();
153 |
154 | $response = $this->json('POST', '/api/orders', [
155 | 'stripe' => [
156 | 'token' => 'random token'
157 | ],
158 | 'order' => [
159 | 'cart_token' => $cart->token,
160 | 'customer_email' => 'email@gmail.com',
161 | 'shipping_first_name' => 'First name',
162 | 'shipping_last_name' => 'Last name',
163 | 'shipping_address_line1' => 'Street 1',
164 | 'shipping_address_line2' => 'Line 2',
165 | 'shipping_address_city' => 'Beverly Hills',
166 | 'shipping_address_region' => 'California',
167 | 'shipping_address_zipcode' => '90210',
168 | 'shipping_address_phone' => '123321123',
169 | 'billing_first_name' => 'First name',
170 | 'billing_last_name' => 'Last name',
171 | 'billing_address_line1' => 'Street 1',
172 | 'billing_address_line2' => 'Line 2',
173 | 'billing_address_city' => 'Beverly Hills',
174 | 'billing_address_region' => 'California',
175 | 'billing_address_zipcode' => '90210',
176 | 'billing_address_phone' => '123321123',
177 | ]
178 | ])
179 | ->assertStatus(200)
180 | ->assertJson(['success' => true])
181 | ->assertJsonStructure([
182 | 'success',
183 | 'data' => $this->orderStructure
184 | ]);
185 |
186 | $order = $this->responseToData($response);
187 |
188 | $response = $this
189 | ->json('GET', '/api/orders/' . $order['token'])
190 | ->assertStatus(200)
191 | ->assertJson(['success' => true])
192 | ->assertJsonStructure([
193 | 'success',
194 | 'data' => $this->orderStructure
195 | ]);
196 |
197 | $orderResponse = $this->responseToData($response);
198 |
199 | $this->assertEquals($order, $orderResponse);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | loadMigrationsFrom(__DIR__ . '/../database/migrations');
19 |
20 | Factory::guessFactoryNamesUsing(
21 | fn (string $modelName) => 'R64\\Checkout\\Database\\Factories\\'.class_basename($modelName).'Factory'
22 | );
23 | }
24 |
25 | protected function getPackageProviders($app)
26 | {
27 | return [
28 | CheckoutServiceProvider::class,
29 | ValidationServiceProvider::class,
30 | StripeServiceProvider::class
31 | ];
32 | }
33 |
34 | protected function getEnvironmentSetUp($app)
35 | {
36 | $app['config']->set('database.default', 'sqlite');
37 | $app['config']->set('database.connections.sqlite', [
38 | 'driver' => 'sqlite',
39 | 'database' => ':memory:',
40 | 'prefix' => '',
41 | ]);
42 |
43 | $app['config']->set('stripe.mock', true);
44 | }
45 |
46 |
47 | protected function responseToData(TestResponse $response)
48 | {
49 | return json_decode($response->getContent(), true)['data'];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Unit/AddressSearchTest.php:
--------------------------------------------------------------------------------
1 | getByPostalCode(12345)->first();
24 |
25 | $this->assertInstanceOf(Address::class, $address);
26 | $this->assertEquals('Baltimore', $address->getCityName());
27 | $this->assertEquals('MD', $address->getStateCode());
28 | $this->assertEquals('Maryland', $address->getStateName());
29 | $this->assertEquals(-76.562940, $address->getLongitude());
30 | $this->assertEquals(39.293810, $address->getLatitude());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------