├── .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 | Build Status 3 | MIT License 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 | --------------------------------------------------------------------------------