├── lang ├── _manifest_exclude ├── pl.yml ├── sv.yml └── es.yml ├── tests └── php │ ├── Cart │ ├── templates │ │ └── CartTestTemplate.ss │ ├── ShoppingCartTest_TestShoppingCartErroringHooksExtension.php │ └── ShoppingCartTest_TestShoppingCartHooksExtension.php │ ├── Admin │ ├── test_products.xls │ ├── test_products.csv │ └── ProductBulkLoaderTest.php │ ├── Extension │ ├── images │ │ ├── ImageA.png │ │ └── ImageB.png │ ├── ViewableCartExtensionTest.php │ └── MemberExtensionTest.php │ ├── Fixtures │ ├── singlecountry.yml │ ├── ShopMembers.yml │ ├── pages │ │ ├── Cart.yml │ │ ├── TermsConditions.yml │ │ ├── Account.yml │ │ ├── Checkout.yml │ │ └── NestedCheckout.yml │ ├── Images.yml │ ├── Pages.yml │ ├── Orders.yml │ ├── Carts.yml │ ├── variations.yml │ └── Addresses.yml │ ├── themes │ └── shoptest │ │ └── templates │ │ └── Page.ss │ ├── Tasks │ ├── FakeCartCleanupTask.php │ └── CartCleanupTaskTest.php │ ├── Page │ ├── ProductTest_FractionalDiscountExtension.php │ └── CheckoutPageTest.php │ ├── ShopTest.php │ ├── Model │ ├── ExtendedTestAddress.php │ ├── OrderTest_TestStatusChangeExtension.php │ ├── Modifiers │ │ ├── OrderModifierTest_TestModifier.php │ │ ├── SimpleShippingModifierTest.php │ │ └── GlobalTaxModifierTest.php │ ├── Product │ │ ├── CustomProduct_OrderItem.php │ │ └── CustomProduct.php │ ├── Variation │ │ └── VariationVersionTest.php │ └── AddressTest.php │ ├── ORM │ ├── FieldType │ │ ├── ShopCountryTest.php │ │ ├── I18nDatetimeTest.php │ │ ├── CanBeFreeCurrencyTest.php │ │ └── ShopCurrencyTest.php │ └── Filters │ │ └── MatchObjectFilterTest.php │ ├── ShopToolsTest.php │ ├── Checkout │ ├── OrderProcessorTest_CustomOrderItem.php │ ├── NestedCheckoutTest.php │ ├── OrderProcessorTest_PlaceFailExtension.php │ └── OrderEmailNotifierTest.php │ ├── ShopUserInfoTest.php │ ├── ShopTestControllerExtension.php │ ├── TestGatewayFactory.php │ ├── Reports │ └── ShopReportTest.php │ └── Forms │ └── AddProductFormTest.php ├── .gitattributes ├── client ├── dist │ ├── css │ │ ├── printorder.css │ │ ├── cartdebug.css │ │ ├── product.css │ │ ├── sidecart.css │ │ ├── cart.css │ │ ├── checkout.css │ │ ├── productcategory.css │ │ ├── packagingslip.css │ │ └── account.css │ ├── images │ │ └── remove.gif │ └── javascript │ │ ├── EcomPayment.js │ │ ├── OrderActionsForm.js │ │ └── CheckoutPage.nojquery.js └── src │ ├── Icons.sketch │ ├── export │ ├── catalog.svg │ ├── zones.svg │ └── cart.svg │ └── config.json ├── .gitignore ├── .tx └── config ├── src ├── Checkout │ ├── Component │ │ ├── BillingAddress.php │ │ ├── ShippingAddress.php │ │ ├── AddressBookBilling.php │ │ ├── AddressBookShipping.php │ │ ├── Notes.php │ │ ├── Terms.php │ │ └── CustomerDetails.php │ ├── CheckoutConfig.php │ ├── Step │ │ ├── AddressBook.php │ │ ├── CheckoutStep.php │ │ ├── Summary.php │ │ ├── PaymentMethod.php │ │ └── ContactDetails.php │ └── SinglePageCheckoutComponentConfig.php ├── Reports │ ├── ShopReportQuery.php │ ├── SideReport │ │ ├── NoImageProducts.php │ │ ├── AllProducts.php │ │ ├── HeavyProducts.php │ │ └── FeaturedProducts.php │ ├── AbandonedCartReport.php │ ├── ShopSalesReport.php │ ├── TaxReport.php │ └── CustomerReport.php ├── Model │ ├── Modifiers │ │ ├── Shipping │ │ │ ├── Free.php │ │ │ ├── Base.php │ │ │ ├── Pickup.php │ │ │ └── Simple.php │ │ ├── SubTotal.php │ │ └── Tax │ │ │ ├── Base.php │ │ │ ├── FlatTax.php │ │ │ └── GlobalTax.php │ ├── Buyable.php │ ├── Variation │ │ └── AttributeValue.php │ └── OrderAttribute.php ├── Extension │ ├── LocationFormPageExtension.php │ ├── PaymentServiceExtension.php │ ├── PaymentExtension.php │ └── ViewableCartExtension.php ├── ORM │ ├── FieldType │ │ ├── CanBeFreeCurrency.php │ │ ├── ShopCountry.php │ │ └── I18nDatetime.php │ └── OrderItemList.php ├── Tasks │ ├── ProductVariationsFromAttributeCombinations.php │ ├── RecalculateAllOrdersTask.php │ ├── PopulateCartTask.php │ ├── CartCleanupTask.php │ ├── CustomersToGroupTask.php │ └── ShopEmailPreviewTask.php ├── Forms │ ├── RestrictionRegionCountryDropdownField.php │ ├── VariationFormValidator.php │ ├── OrderModifierForm.php │ ├── DropdownShopQuantityField.php │ ├── SetLocationForm.php │ ├── OrderActionsFormValidator.php │ ├── CheckoutComponentValidator.php │ └── GridField │ │ └── OrderGridFieldDetailForm_ItemRequest.php ├── Admin │ └── ProductCatalogAdmin.php ├── Page │ ├── ProductController.php │ ├── CartPageController.php │ └── ProductCategoryController.php ├── ShopUserInfo.php └── Dev │ └── ShopDevelopmentAdmin.php ├── _config ├── config.yml ├── logging.yml ├── routes.yml ├── model.yml ├── extensions.yml └── legacy.yml ├── templates └── SilverShop │ ├── Cart │ ├── ShoppingCart_debug.ss │ └── SideCart.ss │ ├── Page │ └── Layout │ │ ├── CheckoutPage_payment.ss │ │ ├── CheckoutPage_createaccount.ss │ │ ├── AccountPage_editprofile.ss │ │ ├── CheckoutPage_membership.ss │ │ ├── AccountPage.ss │ │ ├── CheckoutPage_order.ss │ │ ├── AccountPage_order.ss │ │ ├── ProductCategory.ss │ │ ├── CheckoutPage.ss │ │ ├── CartPage.ss │ │ └── Product.ss │ ├── Includes │ ├── SideBar.ss │ ├── Price.ss │ ├── PriceTag.ss │ ├── OrderHistory.ss │ ├── VariationsTable.ss │ ├── ProductMenu.ss │ ├── ProductGroupPagination.ss │ ├── ProductGroupItem.ss │ └── AccountNavigation.ss │ ├── Model │ ├── Address.ss │ ├── Order_Address.ss │ ├── Order_Content_SubTotals.ss │ ├── Order_Content.ss │ ├── Order_Content_ItemLine.ss │ ├── Order.ss │ ├── Order_Payments.ss │ ├── Order_AdminNotificationEmail.ss │ ├── Order_ReceiptEmail.ss │ └── Order_ConfirmationEmail.ss │ ├── Forms │ └── ShopQuantityField.ss │ ├── Admin │ ├── OrderAdmin_Printable.ss │ ├── OrderAdmin_Notes.ss │ ├── OrderAdmin_Addresses.ss │ ├── OrderAdmin_Customer.ss │ ├── OrderAdmin_Content_SubTotals.ss │ ├── OrderAdmin_Content.ss │ └── OrderAdmin_Content_ItemLine.ss │ └── Dev │ └── ShopDevelopmentAdmin.ss ├── _config.php ├── .htaccess ├── .scrutinizer.yml ├── .github └── workflows │ └── ci.yml ├── .editorconfig ├── phpunit.xml.dist ├── LICENSE └── phpcs.xml.dist /lang/_manifest_exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/php/Cart/templates/CartTestTemplate.ss: -------------------------------------------------------------------------------- 1 | $Cart.Total -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/ export-ignore 2 | docs_user/ export-ignore 3 | -------------------------------------------------------------------------------- /client/dist/css/printorder.css: -------------------------------------------------------------------------------- 1 | #Icons { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | /resources/ 4 | /composer.lock 5 | /app 6 | /public 7 | -------------------------------------------------------------------------------- /client/src/Icons.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvershop/silvershop-core/HEAD/client/src/Icons.sketch -------------------------------------------------------------------------------- /client/dist/images/remove.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvershop/silvershop-core/HEAD/client/dist/images/remove.gif -------------------------------------------------------------------------------- /tests/php/Admin/test_products.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvershop/silvershop-core/HEAD/tests/php/Admin/test_products.xls -------------------------------------------------------------------------------- /tests/php/Extension/images/ImageA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvershop/silvershop-core/HEAD/tests/php/Extension/images/ImageA.png -------------------------------------------------------------------------------- /tests/php/Extension/images/ImageB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvershop/silvershop-core/HEAD/tests/php/Extension/images/ImageB.png -------------------------------------------------------------------------------- /lang/pl.yml: -------------------------------------------------------------------------------- 1 | pl: 2 | SilverShop\Admin\OrdersAdmin: 3 | MENUTITLE: Zamówienia 4 | SilverShop\Forms: 5 | FIELDISREQUIRED: '{name} jest wymagane' 6 | -------------------------------------------------------------------------------- /tests/php/Fixtures/singlecountry.yml: -------------------------------------------------------------------------------- 1 | SilverStripe\SiteConfig\SiteConfig: 2 | config: 3 | Title: Shop Testing Website 4 | AllowedCountries: '["NZ"]' 5 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [silverstripe-shop.lang_2-0] 5 | file_filter = lang/.yml 6 | source_file = lang/en.yml 7 | source_lang = en 8 | type = YML 9 | -------------------------------------------------------------------------------- /tests/php/Fixtures/ShopMembers.yml: -------------------------------------------------------------------------------- 1 | SilverStripe\Security\Member: 2 | jeremyperemy: 3 | FirstName: Jeremy 4 | Surname: Peremy 5 | Email: jeremy@example.com 6 | Password: jeremyperemy 7 | -------------------------------------------------------------------------------- /tests/php/Fixtures/pages/Cart.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Page\CartPage: 2 | cart: 3 | URLSegment: cart 4 | Title: Shopping Cart 5 | MenuTitle: Cart 6 | ShowInMenus: 0 7 | ShowInSearch: 0 8 | -------------------------------------------------------------------------------- /client/src/export/catalog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Checkout/Component/BillingAddress.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | $MetaTags(0) 4 | Debugging ShoppingCart 5 | 6 | 7 | $Content 8 | 9 | 10 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | steps){ 7 | SteppedCheckoutExtension::setupSteps($checkoutsteps); 8 | } 9 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Deny from all 3 | 4 | 5 | Allow from all 6 | 7 | 8 | Deny from all 9 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: true 5 | 6 | build: 7 | nodes: 8 | analysis: 9 | tests: 10 | override: [php-scrutinizer-run] 11 | 12 | filter: 13 | paths: 14 | - src/* 15 | - tests/* 16 | -------------------------------------------------------------------------------- /_config/routes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: silvershop-routes 3 | After: framework/routes#coreroutes 4 | --- 5 | SilverStripe\Control\Director: 6 | rules: 7 | shoppingcart: SilverShop\Cart\ShoppingCartController 8 | dev/shop: SilverShop\Dev\ShopDevelopmentAdmin 9 | -------------------------------------------------------------------------------- /tests/php/Fixtures/pages/TermsConditions.yml: -------------------------------------------------------------------------------- 1 | Page: 2 | termsconditions: 3 | URLSegment: terms-and-conditions 4 | Title: 'Terms and Conditions' 5 | MenuTitle: 'Terms & Conditions' 6 | ShowInMenus: 0 7 | ShowInSearch: 1 8 | Content: 'You agree to...' 9 | -------------------------------------------------------------------------------- /_config/model.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: silvershop-fieldtypes 3 | After: corefieldtypes 4 | --- 5 | SilverStripe\Core\Injector\Injector: 6 | Currency: 7 | class: SilverShop\ORM\FieldType\ShopCurrency 8 | ShopCountry: 9 | class: SilverShop\ORM\FieldType\ShopCountry 10 | -------------------------------------------------------------------------------- /tests/php/themes/shoptest/templates/Page.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% base_tag %> 5 | $Title.ATT 6 | $MetaTags(0) 7 | 8 | 9 |
10 | $Layout 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Reports/ShopReportQuery.php: -------------------------------------------------------------------------------- 1 | This is the account page. It is used for shop users to login and change their member details if they have an account.

' 6 | ShowInMenus: 0 7 | -------------------------------------------------------------------------------- /client/dist/css/cartdebug.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: tahoma; 3 | font-size: 1em; 4 | } 5 | 6 | pre { 7 | display: inline; 8 | font-weight: bold; 9 | color: blue; 10 | } 11 | 12 | .order > ul { 13 | -moz-column-count: 3; 14 | } 15 | 16 | h2 { 17 | margin-top: 2px; 18 | border-bottom: 1px solid grey; 19 | } 20 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CheckoutPage_payment.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/checkout.css") %> 2 | 3 |

$Title

4 |
5 |
6 | <% if $Content %> 7 | $Content 8 | <% end_if %> 9 |
10 | $OrderForm 11 |
12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | ci: 10 | name: CI 11 | uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 12 | with: 13 | composer_require_extra: 14 | phpunit/phpunit:^9.6.22 15 | symfony/http-foundation:^6.4.8 16 | moneyphp/money:^4.6.0 17 | -------------------------------------------------------------------------------- /tests/php/Fixtures/Images.yml: -------------------------------------------------------------------------------- 1 | SilverStripe\Assets\Image: 2 | imageA: 3 | Title: 'Test Image A' 4 | Name: ImageA.png 5 | FileFilename: ImageA.png 6 | FileHash: 5018c2cd7f0d2a58b1d5c956db7ced624def6d28 7 | imageB: 8 | Title: 'Test Image B' 9 | Name: ImageB.png 10 | FileFilename: ImageB.png 11 | FileHash: 97670e66d73f828d2d3c885669692af30736f2e5 12 | -------------------------------------------------------------------------------- /tests/php/Tasks/FakeCartCleanupTask.php: -------------------------------------------------------------------------------- 1 | log[] = $msg; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/SideBar.ss: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CheckoutPage_createaccount.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/checkout.css") %> 2 | 3 |

$Title

4 |
5 |
6 | <% if $Content %> 7 | $Content 8 | <% end_if %> 9 |
10 |
11 | $Form 12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/AccountPage_editprofile.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/account.css") %> 2 | 3 | <% include SilverShop\Includes\AccountNavigation %> 4 |
5 | 6 |

7 | <%t SilverShop\Page\AccountPage_EditProfile.Title 'Edit Profile' %> 8 |

9 | 10 | $EditAccountForm 11 | $ChangePasswordForm 12 | 13 |
14 | -------------------------------------------------------------------------------- /client/dist/css/product.css: -------------------------------------------------------------------------------- 1 | #Product { 2 | 3 | } 4 | 5 | .price .value { 6 | font-weight: bold; 7 | font-size: 1.2em; 8 | } 9 | 10 | /* Image placeholder */ 11 | #Product .noimage { 12 | padding: 60px 20px; 13 | height: 40px; 14 | width: 200px; 15 | text-align: center; 16 | background: #eeeeee; 17 | color: #d3d3d3; 18 | display: block; 19 | text-decoration: none; 20 | font-size: 20px; 21 | } 22 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Address.ss: -------------------------------------------------------------------------------- 1 | <% if $Name %>$Name
<% end_if %> 2 | <% if $Company %>$Company
<% end_if %> 3 | <% if $Address %>$Address
<% end_if %> 4 | <% if $AddressLine2 %>$AddressLine2
<% end_if %> 5 | <% if $City %>$City
<% end_if %> 6 | <% if $PostalCode %>$PostalCode
<% end_if %> 7 | <% if $State %>$State
<% end_if %> 8 | <% if $Country %>$Country
<% end_if %> 9 | <% if $Phone %>$Phone<% end_if %> 10 | -------------------------------------------------------------------------------- /tests/php/Page/ProductTest_FractionalDiscountExtension.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ProductTest_FractionalDiscountExtension extends Extension implements TestOnly 12 | { 13 | public function updateSellingPrice(&$price): void 14 | { 15 | $price -= 0.015; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/export/zones.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/php/Fixtures/pages/Checkout.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Page\CheckoutPage: 2 | checkout: 3 | URLSegment: checkout 4 | Title: Checkout 5 | Content: '

This is the checkout page. The order summary and order form appear below this content.

' 6 | PurchaseComplete: '

Your purchase is complete.

' 7 | ChequeMessage: '

Please note: Your goods will not be dispatched until we receive your payment.

' 8 | ShowInMenus: 0 9 | ShowInSearch: 0 10 | -------------------------------------------------------------------------------- /client/dist/css/sidecart.css: -------------------------------------------------------------------------------- 1 | .sidecart { 2 | } 3 | 4 | .sidecart .item { 5 | clear: both; 6 | position: relative; 7 | padding-right: 16px; 8 | } 9 | 10 | .sidecart .image { 11 | float: left; 12 | margin-right: 1em; 13 | } 14 | 15 | .sidecart .remove { 16 | position: absolute; 17 | top: 0; 18 | right: 0; 19 | } 20 | 21 | .sidecart p { 22 | margin-bottom: 0.1em; 23 | } 24 | 25 | .sidecart .quantity { 26 | font-weight: bold; 27 | } 28 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_Address.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
<%t SilverShop\Model\Order.ShipTo "Ship To" %><%t SilverShop\Model\Order.BillTo "Bill To" %>
$getShippingAddress$getBillingAddress
15 | -------------------------------------------------------------------------------- /tests/php/ShopTest.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | $PriceRange.Min.Nice 4 | <% if $PriceRange.HasRange %> 5 | - $PriceRange.Max.Nice 6 | <% end_if %> 7 | $Price.Currency 8 |
9 | <% else_if $Price %> 10 |
11 | $Price.Nice $Price.Currency 12 |
13 | <% end_if %> -------------------------------------------------------------------------------- /src/Model/Modifiers/Shipping/Free.php: -------------------------------------------------------------------------------- 1 | 2 | ">- 4 | $Field 5 | ">+ 7 | 8 | -------------------------------------------------------------------------------- /src/Extension/LocationFormPageExtension.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class LocationFormPageExtension extends Extension 12 | { 13 | private static array $allowed_actions = [ 14 | 'SetLocationForm', 15 | ]; 16 | 17 | public function SetLocationForm(): SetLocationForm 18 | { 19 | return SetLocationForm::create($this->owner); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Extension/PaymentServiceExtension.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class PaymentServiceExtension extends Extension 13 | { 14 | public function updatePartialPayment($newPayment, $originalPayment): void 15 | { 16 | $newPayment->OrderID = $originalPayment->OrderID; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CheckoutPage_membership.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/checkout.css") %> 2 | 3 |

$Title

4 |
5 |
6 | <% if $Content %> 7 | $Content 8 | <% end_if %> 9 |
10 |
11 | $Form 12 |
13 | 17 |
18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,js,json,css,scss,eslintrc}] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | # The indent size used in the package.json file cannot be changed: 18 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 19 | -------------------------------------------------------------------------------- /src/ORM/FieldType/CanBeFreeCurrency.php: -------------------------------------------------------------------------------- 1 | value == 0) { 15 | return _t('SilverShop\ORM\FieldType\ShopCurrency.Free', 'FREE'); 16 | } 17 | return parent::Nice(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Shipping/Base.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Printable.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% base_tag %> 4 | $MetaTags 5 | <% include SilverShop\Includes\OrderReceiptStyle %> 6 | 7 | 8 |
9 |

10 | <%t SilverShop\Admin\OrdersAdmin.ReceiptTitle "{SiteTitle} Order {OrderNo}" SiteTitle=$SiteConfig.Title OrderNo=$Reference %> 11 |

12 | <% include SilverShop\Model\Order %> 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/php/Fixtures/pages/NestedCheckout.yml: -------------------------------------------------------------------------------- 1 | Page: 2 | shop: 3 | Title: Shop 4 | URLSegment: myshop 5 | 6 | SilverShop\Page\CheckoutPage: 7 | checkout: 8 | URLSegment: checkout 9 | Title: Checkout 10 | Content: '

This is the checkout page. The order summary and order form appear below this content.

' 11 | PurchaseComplete: '

Your purchase is complete.

' 12 | ChequeMessage: '

Please note: Your goods will not be dispatched until we receive your payment.

' 13 | ShowInMenus: 0 14 | ShowInSearch: 0 15 | Parent: =>Page.shop 16 | -------------------------------------------------------------------------------- /tests/php/Model/ExtendedTestAddress.php: -------------------------------------------------------------------------------- 1 | setValue("ABC"); 14 | $this->assertEquals("ABC", $shopCountry->forTemplate()); 15 | $shopCountry->setValue("NZ"); 16 | $this->assertEquals("New Zealand", $shopCountry->forTemplate()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/php/ORM/FieldType/I18nDatetimeTest.php: -------------------------------------------------------------------------------- 1 | setValue('2012-11-21 11:54:13'); 15 | 16 | $i18nDatetime->Nice(); 17 | $i18nDatetime->NiceDate(); 18 | $i18nDatetime->Nice24(); 19 | 20 | $this->markTestIncomplete('assertions!'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/php/ShopToolsTest.php: -------------------------------------------------------------------------------- 1 | setCurrency("NZD"); 20 | $dbMoney->setLocale("en_NZ"); 21 | $this->assertEquals($dbMoney->Nice(), "$12,345.67"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tasks/ProductVariationsFromAttributeCombinations.php: -------------------------------------------------------------------------------- 1 | count()) { 18 | return; 19 | } 20 | 21 | foreach ($products as $product) { 22 | $product->generateVariationsFromAttributes(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | tests/php/ 9 | 10 | 11 | 12 | 13 | tests/php 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/AccountPage.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/account.css") %> 2 | 3 | <% include SilverShop\Includes\AccountNavigation %> 4 |
5 | $Content 6 |

<%t SilverShop\Page\AccountPage.PastOrders 'Past Orders' %>

7 | <% with $Member %> 8 | <% if $PastOrders %> 9 | <% include SilverShop\Includes\OrderHistory %> 10 | <% else %> 11 |

<%t SilverShop\Page\AccountPage.NoPastOrders 'No past orders found.' %>

12 | <% end_if %> 13 | <% end_with %> 14 |
15 | -------------------------------------------------------------------------------- /tests/php/Model/OrderTest_TestStatusChangeExtension.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class OrderTest_TestStatusChangeExtension extends Extension implements TestOnly 12 | { 13 | public static $stack = []; 14 | 15 | public static function reset(): void 16 | { 17 | self::$stack = []; 18 | } 19 | 20 | public function onStatusChange($fromStatus, $toStatus): void 21 | { 22 | self::$stack[] = [ 23 | $fromStatus => $toStatus 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/php/ORM/FieldType/CanBeFreeCurrencyTest.php: -------------------------------------------------------------------------------- 1 | setValue(20000); 14 | $this->assertEquals("$20,000.00", $canBeFreeCurrency->Nice()); 15 | $canBeFreeCurrency->setValue(0); 16 | $this->assertEquals("FREE", $canBeFreeCurrency->Nice()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lang/sv.yml: -------------------------------------------------------------------------------- 1 | sv: 2 | SilverShop\Admin\OrdersAdmin: 3 | MENUTITLE: Beställningar 4 | SilverShop\Admin\ProductCatalogAdmin: 5 | MENUTITLE: Katalog 6 | SilverShop\Admin\ZoneAdmin: 7 | MENUTITLE: Zones 8 | SilverShop\Forms: 9 | FIELDISREQUIRED: '{name} är obligatoriskt' 10 | SilverShop\Page\AccountPage: 11 | AddressBook: 'Adressbok' 12 | DefaultTitle: Konto 13 | LogOut: 'Logga ut' 14 | MemberEmail: Email 15 | MemberLastVisit: 'Senaste besök' 16 | MemberName: Namn 17 | MemberSince: 'Medlem sedan' 18 | NumberOfOrders: 'Antal beställningar' 19 | SilverShop\Reports\AbandonedCartReport: 20 | Title: 'Övergivna varukorgar' 21 | -------------------------------------------------------------------------------- /tests/php/Checkout/OrderProcessorTest_CustomOrderItem.php: -------------------------------------------------------------------------------- 1 | 'Boolean' 13 | ]; 14 | 15 | private static string $table_name = 'SilverShop_Test_CustomOrderItem'; 16 | 17 | public function onPlacement(): void 18 | { 19 | parent::onPlacement(); 20 | $this->IsPlaced = true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Shipping/Pickup.php: -------------------------------------------------------------------------------- 1 | 'Ignored', 21 | ]; 22 | 23 | private static array $casting = [ 24 | 'TableValue' => CanBeFreeCurrency::class, 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Notes.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
6 |

<%t SilverShop\Model\Order.db_Notes "Notes" %>

7 |
$Notes
21 |
22 | -------------------------------------------------------------------------------- /tests/php/Fixtures/Pages.yml: -------------------------------------------------------------------------------- 1 | Page: 2 | termsandconditions: 3 | Title: Terms and Conditions 4 | URLSegment: terms-and-conditions 5 | Content: Test terms and conditions. 6 | 7 | SilverShop\Page\CheckoutPage: 8 | checkout: 9 | Title: Checkout 10 | URLSegment: checkout 11 | 12 | SilverShop\Page\CartPage: 13 | cart: 14 | Title: Cart 15 | URLSegment: cart 16 | 17 | SilverShop\Page\AccountPage: 18 | accountpage: 19 | Title: Account 20 | URLSegment: account 21 | 22 | SilverStripe\SiteConfig\SiteConfig: 23 | config: 24 | Title: Shop Testing Website 25 | ShopClosed: 0 26 | TermsPage: =>Page.termsandconditions 27 | #CustomerGroup: =>Group.customers 28 | -------------------------------------------------------------------------------- /src/Forms/RestrictionRegionCountryDropdownField.php: -------------------------------------------------------------------------------- 1 | getCountriesList(true); 15 | parent::__construct($name, $title, $source, $value); 16 | $this->setHasEmptyDefault(true); 17 | $this->setEmptyString(static::config()->get('defaultname')); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CheckoutPage_order.ss: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if $Order %> 4 | <% with $Order %> 5 |

<%t SilverShop\Model\Order.OrderHeadline "Order #{OrderNo} {OrderDate}" OrderNo=$Reference OrderDate=$Created.Nice %>

6 | <% end_with %> 7 | <% end_if %> 8 | <% if $Message %> 9 |

$Message

10 | <% end_if %> 11 | <% if $Order %> 12 | <% with $Order %> 13 | <% include SilverShop\Model\Order %> 14 | <% end_with %> 15 | $Form 16 | <% end_if %> 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/Model/Modifiers/SubTotal.php: -------------------------------------------------------------------------------- 1 | 'Ignored', 15 | ]; 16 | 17 | private static string $singular_name = 'Sub Total'; 18 | 19 | private static string $plural_name = 'Sub Totals'; 20 | 21 | private static string $table_name = 'SilverShop_SubTotalModifier'; 22 | 23 | public function value($incoming): int|float 24 | { 25 | return $this->Amount = $incoming; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/php/ORM/FieldType/ShopCurrencyTest.php: -------------------------------------------------------------------------------- 1 | currency_symbol = "X"; 13 | ShopCurrency::config()->decimal_delimiter = "|"; 14 | ShopCurrency::config()->thousand_delimiter = "-"; 15 | ShopCurrency::config()->negative_value_format = "- %s"; 16 | 17 | $shopCurrency = ShopCurrency::create("Price"); 18 | $shopCurrency->setValue(-12345.56); 19 | $this->assertEquals("- X12-345|56", $shopCurrency->Nice()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/AccountPage_order.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/account.css") %> 2 | 3 | <% include SilverShop\Includes\AccountNavigation %> 4 |
5 | <% if $Order %> 6 | <% with $Order %> 7 |

<%t SilverShop\Model\Order.OrderHeadline "Order #{OrderNo} {OrderDate}" OrderNo=$Reference OrderDate=$Created.Nice %>

8 | <% end_with %> 9 | <% end_if %> 10 | <% if $Message %> 11 |

$Message

12 | <% end_if %> 13 | <% if $Order %> 14 | <% with $Order %> 15 | <% include SilverShop\Model\Order %> 16 | <% end_with %> 17 | $ActionsForm 18 | <% end_if %> 19 |
20 | -------------------------------------------------------------------------------- /tests/php/ShopUserInfoTest.php: -------------------------------------------------------------------------------- 1 | setLocation( 13 | [ 14 | 'Country' => 'NZ', 15 | 'State' => 'Wellington', 16 | 'City' => 'Newton', 17 | ] 18 | ); 19 | 20 | $location = ShopUserInfo::singleton()->getAddress(); 21 | 22 | $this->assertEquals($location->Country, 'NZ'); 23 | $this->assertEquals($location->State, 'Wellington'); 24 | $this->assertEquals($location->City, 'Newton'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/dist/javascript/EcomPayment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * helps in EcommercePayment Selection 3 | * 4 | **/ 5 | (function ($) { 6 | $(window).load(function () { 7 | EcomPayment.init(); 8 | }); 9 | })(jQuery); 10 | 11 | var EcomPayment = { 12 | 13 | init: function () { 14 | var paymentInputs = $('#PaymentMethod input[type=radio]'); 15 | var methodFields = $('div.paymentfields'); 16 | 17 | methodFields.hide(); 18 | 19 | paymentInputs.each(function (e) { 20 | if ($(this).attr('checked') == true) { 21 | $('#MethodFields_' + $(this).attr('value')).show(); 22 | } 23 | }); 24 | 25 | paymentInputs.click(function (e) { 26 | methodFields.hide(); 27 | $('#MethodFields_' + $(this).attr('value')).show(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Forms/VariationFormValidator.php: -------------------------------------------------------------------------------- 1 | form->getBuyable($_POST)) { 17 | $this->validationError( 18 | '', 19 | _t( 20 | 'SilverShop\Forms\VariationForm.ProductNotAvailable', 21 | 'This product is not available with the selected options.' 22 | ) 23 | ); 24 | 25 | $valid = false; 26 | } 27 | 28 | return $valid; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/PriceTag.ss: -------------------------------------------------------------------------------- 1 |
2 | <% if $DiscountedPrice %> 3 | 4 | $Price.Symbol 5 | $Price.Main 6 | $Price.Fractional 7 | $Price.CurrencyCode 8 | 9 | $DiscountedPrice.Nice <%t SilverShop\Includes\PriceTag.SAVE "Save" %>: $DiscountedPrice.Savings 10 | <% else %> 11 | $Price.Nice 12 | <% end_if %> 13 | <% if $RecommendedPrice %>$RecommendedPrice.Nice<% end_if %> 14 |
15 | -------------------------------------------------------------------------------- /tests/php/Cart/ShoppingCartTest_TestShoppingCartErroringHooksExtension.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ShoppingCartTest_TestShoppingCartErroringHooksExtension extends Extension implements TestOnly 13 | { 14 | public function beforeSetQuantity($buyable, $quantity, $filter): void 15 | { 16 | if ($quantity > 10) { 17 | throw new Exception('Invalid quantity'); 18 | } 19 | } 20 | 21 | public function afterAdd($item, $buyable, $quantity, $filter): void 22 | { 23 | if ($item->Quantity > 1) { 24 | throw new Exception('Invalid quantity'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/php/Checkout/NestedCheckoutTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 22 | Director::baseURL() . 'myshop/checkout', 23 | CheckoutPage::find_link(), 24 | 'Link is: ' . CheckoutPage::find_link() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Forms/OrderModifierForm.php: -------------------------------------------------------------------------------- 1 | redirect(CheckoutPage::find_link()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/php/ShopTestControllerExtension.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ShopTestControllerExtension extends Extension implements TestOnly 13 | { 14 | public function onAfterInit(): void 15 | { 16 | $this->owner->response->addHeader( 17 | 'X-TestPageClass', 18 | get_class($this->owner) 19 | ); 20 | $params = $this->owner->getURLParams(); 21 | if (isset($params['Action'])) { 22 | $this->owner->response->addHeader('X-TestPageAction', $params['Action']); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/php/Checkout/OrderProcessorTest_PlaceFailExtension.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class OrderProcessorTest_PlaceFailExtension extends Extension implements TestOnly 13 | { 14 | private bool $willFail = false; 15 | 16 | public function onPlaceOrder(): void 17 | { 18 | // flag this order to fail 19 | $this->willFail = true; 20 | } 21 | 22 | public function onAfterWrite(): void 23 | { 24 | // fail after writing, so that we can test if DB rollback works as intended 25 | if ($this->willFail) { 26 | user_error('Order failed'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/ProductCategory.ss: -------------------------------------------------------------------------------- 1 |
2 |

$Title

3 | <% if $Content %> 4 |
5 | $Content 6 |
7 | <% end_if %> 8 | 9 | <% if $Products %> 10 |
11 | <%-- include Sorter --%> 12 |
13 |
    14 | <% loop $Products %> 15 | <% include SilverShop\Includes\ProductGroupItem %> 16 | <% end_loop %> 17 |
18 |
19 | <% include SilverShop\Includes\ProductGroupPagination %> 20 |
21 | <% end_if %> 22 |
23 | <% include SilverShop\Includes\SideBar %> 24 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_Content_SubTotals.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%t SilverShop\Model\Order.SubTotal "Sub-total" %> 4 | $SubTotal.Nice 5 | 6 | <% loop $Modifiers %> 7 | <% if $ShowInTable %> 8 | 9 | $TableTitle 10 | $TableValue.Nice 11 | 12 | <% end_if %> 13 | <% end_loop %> 14 | 15 | <%t SilverShop\Model\Order.Total "Total" %> 16 | $Total.Nice $Currency 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ORM/FieldType/ShopCountry.php: -------------------------------------------------------------------------------- 1 | Nice(); 19 | } 20 | 21 | /** 22 | * Convert ISO abbreviation to full, translated country name 23 | */ 24 | public function Nice(): string 25 | { 26 | return IntlLocales::singleton()->countryName($this->value); 27 | } 28 | 29 | public function XML(): string 30 | { 31 | return Convert::raw2xml($this->Nice()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/php/Model/Modifiers/OrderModifierTest_TestModifier.php: -------------------------------------------------------------------------------- 1 | willFail = true; 19 | } 20 | return self::$value; 21 | } 22 | 23 | protected function onAfterWrite(): void 24 | { 25 | parent::onAfterWrite(); 26 | if ($this->willFail) { 27 | user_error('Modifier failure!'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Admin/ProductCatalogAdmin.php: -------------------------------------------------------------------------------- 1 | ProductBulkLoader::class, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/Page/ProductController.php: -------------------------------------------------------------------------------- 1 | create($this->getFormClass(), $this, 'Form'); 19 | $this->extend('updateForm', $form); 20 | return $form; 21 | } 22 | 23 | /** 24 | * Get the form class to use to edit this product in the frontend 25 | * @return string FQCN 26 | */ 27 | public function getFormClass(): string 28 | { 29 | $formClass = AddProductForm::class; 30 | $this->extend('updateFormClass', $formClass); 31 | return $formClass; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CheckoutPage.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/checkout.css") %> 2 | 3 |

$Title

4 |
5 |
6 | 7 | <% if $PaymentErrorMessage %> 8 |

9 | <%t SilverShop\Page\CheckoutPage.PaymentErrorMessage 'Received error from payment gateway:' %> 10 | $PaymentErrorMessage 11 |

12 | <% end_if %> 13 | 14 | <% if $Content %> 15 | $Content 16 | <% end_if %> 17 |
18 | <% if $Cart %> 19 | <% with $Cart %> 20 | <% include SilverShop\Cart\Cart ShowSubtotals=true %> 21 | <% end_with %> 22 | $OrderForm 23 | <% else %> 24 |

<%t SilverShop\Cart\ShoppingCart.NoItems "There are no items in your cart." %>

25 | <% end_if %> 26 |
27 | -------------------------------------------------------------------------------- /tests/php/TestGatewayFactory.php: -------------------------------------------------------------------------------- 1 | filter('ImageID', 0)->sort('Title', 'ASC'); 29 | } 30 | 31 | public function columns(): array 32 | { 33 | return [ 34 | 'Title' => [ 35 | 'title' => 'Title', 36 | 'link' => true, 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/dist/css/checkout.css: -------------------------------------------------------------------------------- 1 | /** Minimal Checkout Page Styling **/ 2 | 3 | #OrderForm_OrderForm { 4 | clear: left; 5 | } 6 | 7 | /* CheckoutPage form styling */ 8 | #LeftOrder, #RightOrder { 9 | float: left; 10 | 11 | } 12 | 13 | #LeftOrder { 14 | width: 50%; 15 | } 16 | 17 | #RightOrder { 18 | width: 50%; 19 | } 20 | 21 | #LeftOrder .CompositeField { 22 | margin-right: 10px; 23 | } 24 | 25 | #BottomOrder { 26 | clear: both; 27 | } 28 | 29 | #Checkout form label.left { 30 | float: left; 31 | width: 30%; 32 | font-weight: bold; 33 | } 34 | 35 | #Checkout form input.nolabel { 36 | margin-left: 30%; 37 | } 38 | 39 | #Checkout form input.text { 40 | width: 50%; 41 | } 42 | 43 | #Checkout form #PaymentMethod li { 44 | list-style: none; 45 | } 46 | 47 | #Checkout form #Amount { 48 | font-size: 1.5em; 49 | } 50 | 51 | #Checkout #Notes .middleColumn textarea { 52 | clear: left; 53 | width: 100%; 54 | resize: vertical; 55 | } 56 | 57 | table.ordercontent { 58 | width: 100%; 59 | } 60 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Addresses.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
6 |

<%t SilverShop\Model\Address.SINGULARNAME "Address" %>

7 |
<%t SilverShop\Model\Order.ShipTo "Ship To" %><%t SilverShop\Model\Order.BillTo "Bill To" %>
$getShippingAddress$getBillingAddress
26 |
27 | -------------------------------------------------------------------------------- /tests/php/Model/Modifiers/SimpleShippingModifierTest.php: -------------------------------------------------------------------------------- 1 | set(Simple::class, 'default_charge', 10) 20 | ->set( 21 | Simple::class, 22 | 'charges_for_countries', 23 | [ 24 | 'NZ' => 5, 25 | 'UK' => 20, 26 | ] 27 | ); 28 | } 29 | 30 | public function testShippingCalculation(): void 31 | { 32 | $simple = Simple::create(); 33 | $this->assertEquals(10, $simple->value(100)); 34 | $this->assertEquals(110, $simple->modify(100)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_Content.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% loop $Items %> 18 | <% include SilverShop\Model\Order_Content_ItemLine %> 19 | <% end_loop %> 20 | 21 | <% include SilverShop\Model\Order_Content_SubTotals %> 22 |
<%t SilverShop\Page\Product.SINGULARNAME "Product" %><%t SilverShop\Model\Order.UnitPrice "Unit Price" %><%t SilverShop\Model\Order.Quantity "Quantity" %><%t SilverShop\Model\Order.TotalPriceWithCurrency "Total Price ({Currency})" Currency=$Currency %>
23 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_Content_ItemLine.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% if $Image %> 4 | "> 5 | $Buyable.Title 6 | 7 | <% end_if %> 8 | 9 | 10 | <% if $Link %> 11 | ">$TableTitle 12 | <% else %> 13 | $TableTitle 14 | <% end_if %> 15 | <% if $SubTitle %> 16 | $SubTitle 17 | <% end_if %> 18 | 19 | $UnitPrice.Nice 20 | $Quantity 21 | $Total.Nice 22 | 23 | -------------------------------------------------------------------------------- /lang/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | SilverShop\Admin\OrdersAdmin: 3 | MENUTITLE: Pedidos 4 | SilverShop\Admin\ProductCatalogAdmin: 5 | MENUTITLE: Catálogo 6 | SilverShop\Admin\ZoneAdmin: 7 | MENUTITLE: Zonas 8 | SilverShop\Forms: 9 | FIELDISREQUIRED: 'Se requiere {name}' 10 | SilverShop\Page\AccountPage: 11 | AddressBook: 'Agenda' 12 | DESCRIPTION: "Permite al usuario ver los detalles de su cuenta y historial de pedidos" 13 | DefaultTitle: Cuenta 14 | EditProfile: 'Editar perfil' 15 | LogOut: 'Salir' 16 | MemberEmail: Email 17 | MemberLastVisit: 'Última Visita' 18 | MemberName: Nombre 19 | MemberSince: 'Socio Desde' 20 | NoPastOrders: 'No se encontraron pedidos anteriores.' 21 | NumberOfOrders: 'Cantidad de pedidos' 22 | PLURALNAME: 'Páginas de la cuenta' 23 | PastOrders: 'Historial de pedidos' 24 | Title: 'Mi cuenta' 25 | SilverShop\Reports\AbandonedCartReport: 26 | Description: "Monitorear cestas abandonadas durante un periodo determinado. Agrupar resultados por año, mes o día." 27 | Title: 'Cestas abandonadas' 28 | -------------------------------------------------------------------------------- /src/Forms/DropdownShopQuantityField.php: -------------------------------------------------------------------------------- 1 | config()->max; $r++) { 25 | $qtyArray[$r] = $r; 26 | } 27 | 28 | return DropdownField::create( 29 | $this->MainID() . '_Quantity', 30 | // this title currently doesn't show up in the front end, better assign a translation anyway. 31 | _t('SilverShop\Model\Order.Quantity', "Quantity"), 32 | $qtyArray, 33 | ($this->item->Quantity) ? $this->item->Quantity : "" 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Reports/SideReport/AllProducts.php: -------------------------------------------------------------------------------- 1 | sort('Title'); 34 | } 35 | 36 | public function columns(): array 37 | { 38 | return [ 39 | 'Title' => [ 40 | 'title' => 'Title', 41 | 'link' => true, 42 | ], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Checkout/Component/Notes.php: -------------------------------------------------------------------------------- 1 | Notes = $data['Notes']; 27 | } 28 | //TODO: save this to an order log 29 | 30 | $order->write(); 31 | return $order; 32 | } 33 | 34 | public function getData(Order $order): array 35 | { 36 | return [ 37 | 'Notes' => $order->Notes, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Tax/Base.php: -------------------------------------------------------------------------------- 1 | 'Double', 16 | ]; 17 | 18 | private static array $defaults = [ 19 | 'Rate' => 0.15 //15% tax 20 | ]; 21 | 22 | private static string $singular_name = 'Tax'; 23 | 24 | private static string $plural_name = 'Taxes'; 25 | 26 | private static string $table_name = 'SilverShop_TaxModifier'; 27 | 28 | public function getTableTitle(): string 29 | { 30 | $title = parent::getTableTitle(); 31 | if ($this->Rate) { 32 | $title .= ' ' . _t( 33 | __CLASS__ . '.AtRate', 34 | '@ {Rate}%', 35 | '', 36 | ['Rate' => number_format($this->Rate * 100, 1)] 37 | ); 38 | } 39 | return $title; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/php/Model/Modifiers/GlobalTaxModifierTest.php: -------------------------------------------------------------------------------- 1 | set( 17 | Order::class, 18 | 'modifiers', 19 | [ 20 | GlobalTax::class 21 | ] 22 | )->set( 23 | GlobalTax::class, 24 | 'country_rates', 25 | [ 26 | 'NZ' => ['rate' => 0.15, 'name' => 'GST', 'exclusive' => false], 27 | 'UK' => ['rate' => 0.175, 'name' => 'VAT', 'exclusive' => true], 28 | ] 29 | ); 30 | } 31 | 32 | public function testModification(): void 33 | { 34 | $globalTax = GlobalTax::create(); 35 | $this->assertEquals(15, $globalTax->value(100)); //15% tax default 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/OrderHistory.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% loop $PastOrders %> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | <% end_loop %> 27 | 28 |
<%t SilverShop\Model\Order.db_Reference 'Reference' %><%t SilverShop\Model\Order.Date 'Date' %><%t SilverShop\Model\Order.has_many_Items 'Items' %><%t SilverShop\Model\Order.Total 'Total' %><%t SilverShop\Model\Order.db_Status 'Status' %>
$Reference$Created.Nice$Items.Quantity$Total.Nice$StatusI18N 21 | 22 | <%t SilverShop\Generic.View 'view' %> 23 | 24 |
29 | -------------------------------------------------------------------------------- /src/Reports/SideReport/HeavyProducts.php: -------------------------------------------------------------------------------- 1 | filter('Weight:GreaterThan', 10)->sort('Weight', 'ASC'); 29 | } 30 | 31 | public function columns(): array 32 | { 33 | return [ 34 | 'Title' => [ 35 | 'title' => 'Title', 36 | 'link' => true, 37 | ], 38 | 'Weight' => [ 39 | 'title' => 'Weight', 40 | ], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/php/Extension/ViewableCartExtensionTest.php: -------------------------------------------------------------------------------- 1 | clear(); 21 | ShopTest::setConfiguration(); 22 | $this->logInWithPermission('ADMIN'); 23 | $this->objFromFixture(Product::class, "socks")->publishSingle(); 24 | } 25 | 26 | function testCart(): void 27 | { 28 | $order = $this->objFromFixture(Order::class, "cart"); 29 | ShoppingCart::singleton()->setCurrent($order); 30 | $page = PageController::create(); 31 | $this->assertEquals("$8.00", (string)$page->renderWith("CartTestTemplate")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Customer.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
6 |

<%t SilverShop\Generic.Customer "Customer" %>

7 |
<%t SilverShop\Page\AccountPage.MemberName "Name" %><%t SilverShop\Page\AccountPage.MemberEmail "Email" %>
$Name 18 | <% if $LatestEmail %> 19 | $LatestEmail 20 | <% end_if %> 21 |
30 |
31 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/CartPage.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/cart.css") %> 2 | 3 |

$Title

4 |
5 | <% if $Content %> 6 | $Content 7 | <% end_if %> 8 |
9 | <% if $Cart %> 10 | 11 | <% if $CartForm %> 12 | $CartForm 13 | <% else %> 14 | <% with $Cart %><% include SilverShop\Cart\Cart Editable=true %><% end_with %> 15 | <% end_if %> 16 | 17 | <% else %> 18 |

<%t SilverShop\Cart\ShoppingCart.NoItems "There are no items in your cart." %>

19 | <% end_if %> 20 |
21 | <% if $ContinueLink %> 22 | 23 | <%t SilverShop\Cart\ShoppingCart.ContinueShopping 'Continue Shopping' %> 24 | 25 | <% end_if %> 26 | <% if $Cart %> 27 | <% if $CheckoutLink %> 28 | 29 | <%t SilverShop\Cart\ShoppingCart.ProceedToCheckout 'Proceed to Checkout' %> 30 | 31 | <% end_if %> 32 | <% end_if %> 33 |
34 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Content_SubTotals.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%t SilverShop\Model\Order.SubTotal "Sub-total" %> 4 | $SubTotal.Nice 5 | 6 | <% loop $Modifiers %> 7 | <% if $ShowInTable %> 8 | 9 | 10 | $TableTitle 11 | <% if $SubTitle %>($SubTitle)<% end_if %> 12 | 13 | $TableValue.Nice 14 | 15 | <% end_if %> 16 | <% end_loop %> 17 | 18 | <%t SilverShop\Model\Order.Total "Total" %> 19 | $Total.Nice $Currency 20 | 21 | <% if $TotalOutstanding %> 22 | 23 | <%t SilverShop\Model\Order.Outstanding "Outstanding" %> 24 | $TotalOutstanding.Nice $Currency 25 | 26 | <% end_if %> 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/php/Reports/ShopReportTest.php: -------------------------------------------------------------------------------- 1 | sourceRecords([]); 16 | $records = $shopSalesReport->sourceRecords(['Grouping' => 'Year']); 17 | $records = $shopSalesReport->sourceRecords(['Grouping' => 'Month']); 18 | $records = $shopSalesReport->sourceRecords(['Grouping' => 'Week']); 19 | $records = $shopSalesReport->sourceRecords( 20 | [ 21 | 'Grouping' => 'Day', 22 | 'StartPeriod' => 'May 1, 2010', 23 | 'EndPeriod' => 'May 16, 2111', 24 | ] 25 | ); 26 | $record = $records->first(); 27 | $this->assertEquals("02 October 2012 - Tuesday", $record->FilterPeriod); 28 | $this->assertEquals(1, $record->Count, "One sale on this day"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Content.ss: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% loop $Items %> 19 | <% include SilverShop\Admin\OrderAdmin_Content_ItemLine %> 20 | <% end_loop %> 21 | 22 | <% include SilverShop\Admin\OrderAdmin_Content_SubTotals %> 23 |
6 |

<%t SilverShop\Model\OrderItem.PLURALNAME "Items" %>

7 |
<%t SilverShop\Page\Product.SINGULARNAME "Product" %><%t SilverShop\Model\Order.UnitPrice "Unit Price" %><%t SilverShop\Model\Order.Quantity "Quantity" %><%t SilverShop\Model\Order.TotalPriceWithCurrency "Total Price ({Currency})" Currency=$Currency %>
24 |
25 | -------------------------------------------------------------------------------- /client/dist/css/productcategory.css: -------------------------------------------------------------------------------- 1 | .category { 2 | overflow-x: hidden; 3 | } 4 | 5 | .sortOptions .current { 6 | text-decoration: none; 7 | font-weight: bold; 8 | } 9 | 10 | .productList { 11 | padding: 0; 12 | margin: 0; 13 | } 14 | 15 | /* 3) Product Settings */ 16 | 17 | .productItem { 18 | list-style: none; 19 | float: left; 20 | width: 140px; 21 | height: 250px; 22 | overflow: hidden; 23 | margin: 0 5px 5px 0; 24 | padding: 5px; 25 | } 26 | 27 | .productItem .productActions { 28 | padding: 0; 29 | margin: 0; 30 | } 31 | 32 | .productItem .productActions li { 33 | list-style: none; 34 | padding: 0; 35 | margin: 0; 36 | } 37 | 38 | .resultsBar .resultsShowing, 39 | .resultsBar .resultsShowing { 40 | float: right; 41 | width: 50%; 42 | text-align: right; 43 | } 44 | 45 | /* Image placeholder */ 46 | .productItem a.noimage { 47 | padding: 40px 20px; 48 | height: 20px; 49 | text-align: center; 50 | background: #eeeeee; 51 | color: #d3d3d3; 52 | display: block; 53 | text-decoration: none; 54 | font-size: 20px; 55 | -webkit-border-radius: 5px; 56 | -moz-border-radius: 5px; 57 | border-radius: 5px; 58 | } 59 | -------------------------------------------------------------------------------- /src/Checkout/Step/AddressBook.php: -------------------------------------------------------------------------------- 1 | addComponent(AddressBookShipping::create()); 25 | 26 | return $checkoutComponentConfig; 27 | } 28 | 29 | public function billingconfig(): CheckoutComponentConfig 30 | { 31 | $checkoutComponentConfig = CheckoutComponentConfig::create(ShoppingCart::curr()); 32 | $checkoutComponentConfig->addComponent(AddressBookBilling::create()); 33 | 34 | return $checkoutComponentConfig; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD2 License 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/VariationsTable.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% if $canPurchase %> 6 | 7 | <% end_if %> 8 | 9 | <% loop $Variations %> 10 | 11 | 12 | 13 | 27 | 28 | <% end_loop %> 29 |
<%t SilverShop\Model\ProductVariation.SINGULARNAME "Variation" %><%t SilverShop\Page\Product.Price "Price" %><%t SilverShop\Model\Order.Quantity "Quantity" %>
$Title.XML$Price.Nice $Currency 14 | <% if $canPurchase %> 15 | <% if $IsInCart %> 16 | <% with $Item %> 17 | $QuantityField 18 | <% end_with %> 19 | <% else %> 20 | "> 21 | <%t SilverShop\Page\Product.AddToCart "Add to Cart" %> 22 | 23 | <% end_if %> 24 | 25 | <% end_if %> 26 |
30 | -------------------------------------------------------------------------------- /src/Tasks/RecalculateAllOrdersTask.php: -------------------------------------------------------------------------------- 1 | '; 24 | 25 | //TODO: include order total calculation, once that gets written 26 | //TODO: figure out how to make this run faster 27 | //TODO: better memory managment...the destroy calls are not enough it appears. 28 | 29 | if ($orders = Order::get()) { 30 | echo $br . 'Writing all order items '; 31 | foreach ($orders as $order) { 32 | $order->calculate(); 33 | $order->write(); 34 | } 35 | echo $br . 'done.' . $br; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/order.css") %> 2 | 3 | <%-- As Order.ss is also used in emails, avoid div, paragraph and heading elements --%> 4 | <% include SilverShop\Model\Order_Address %> 5 | <% include SilverShop\Model\Order_Content %> 6 | <% if $Total %> 7 | <% if $Payments %> 8 | <% include SilverShop\Model\Order_Payments %> 9 | <% end_if %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
<%t SilverShop\Model\Order.TotalOutstanding "Total outstanding" %>$TotalOutstanding.Nice
18 | <% end_if %> 19 | <% if $Notes %> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
<%t SilverShop\Model\Order.db_Notes "Notes" %>
$Notes
32 | <% end_if %> 33 | -------------------------------------------------------------------------------- /tests/php/Model/Product/CustomProduct_OrderItem.php: -------------------------------------------------------------------------------- 1 | "Enum('Red,Green,Blue','Red')", 13 | 'Size' => 'Int', 14 | 'Premium' => 'Boolean', 15 | ]; 16 | private static array $defaults = [ 17 | 'Color' => 'Red', 18 | 'Premium' => false, 19 | ]; 20 | private static array $has_one = [ 21 | 'CustomProduct' => CustomProduct::class, 22 | 'Recipient' => Member::class, 23 | ]; 24 | private static string $buyable_relationship = "CustomProduct"; 25 | private static array $required_fields = [ 26 | 'Color', 27 | 'Size', 28 | 'Premium', 29 | 'Recipient', 30 | ]; 31 | private static string $table_name = 'SilverShop_Test_CustomProduct_OrderItem'; 32 | 33 | public function UnitPrice() 34 | { 35 | if ($this->CustomProduct()->exists()) { 36 | return $this->CustomProduct()->Price; 37 | } 38 | return 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/dist/javascript/OrderActionsForm.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $(function () { 3 | // Helper JavaScript to toggle Credit-Card fields, depending on selected gateway 4 | var handleGatewayChanged = function () { 5 | // Get the currently selected gateway 6 | var selected = $("#PaymentMethod input:checked").val(); 7 | // Find credit-card input fields 8 | var ccInput = $("#PaymentMethod").nextAll('.credit-card'); 9 | if (ccInput && ccInput.length > 0) { 10 | // Find gateway lookup data 11 | var lookup = ccInput.find(".gateway-lookup").data("gateways"); 12 | if (lookup && (selected in lookup)) { 13 | // Show the Credit-Card fields if the gateway is in the lookup data 14 | ccInput.show(); 15 | // Hide all CC fields by default 16 | ccInput.find(".field").hide(); 17 | $(lookup[selected]).each(function (i, v) { 18 | // only show the required fields 19 | ccInput.find("[name=" + v + "]").parents(".field").show(); 20 | }); 21 | } else { 22 | ccInput.hide(); 23 | } 24 | } 25 | }; 26 | 27 | $("#PaymentMethod input").on("change", handleGatewayChanged); 28 | handleGatewayChanged(); 29 | }); 30 | })(jQuery); 31 | -------------------------------------------------------------------------------- /templates/SilverShop/Admin/OrderAdmin_Content_ItemLine.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% if $Buyable && $Buyable.Image %> 4 |
5 | "> 6 | $Buyable.Title 7 | 8 |
9 | <% end_if %> 10 | 11 | 12 | 13 | <% if $Link %> 14 | $TableTitle 15 | <% else %> 16 | $TableTitle 17 | <% end_if %> 18 | 19 | <% if $SubTitle %>
$SubTitle
<% end_if %> 20 | <% if $Buyable.InternalItemID %>
<%t SilverShop\Page\Product.ProductCodeShort "SKU" %>: $Buyable.InternalItemID
<% end_if %> 21 | 22 | $UnitPrice.Nice 23 | $Quantity 24 | $Total.Nice 25 | 26 | -------------------------------------------------------------------------------- /_config/extensions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: silvershop-extensions 3 | --- 4 | PageController: 5 | extensions: 6 | - SilverShop\Extension\ViewableCartExtension 7 | 8 | SilverShop\Cart\ShoppingCartController: 9 | extensions: 10 | - SilverShop\Extension\ViewableCartExtension 11 | 12 | SilverShop\Model\Order: 13 | extensions: 14 | - SilverStripe\Omnipay\Extensions\Payable 15 | 16 | SilverShop\Page\AccountPageController: 17 | extensions: 18 | - SilverShop\Extension\OrderManipulationExtension 19 | 20 | SilverShop\Page\CheckoutPageController: 21 | extensions: 22 | - SilverShop\Extension\OrderManipulationExtension 23 | 24 | SilverShop\Page\Product: 25 | extensions: 26 | - SilverShop\Extension\ProductVariationsExtension 27 | 28 | SilverStripe\SiteConfig\SiteConfig: 29 | extensions: 30 | - SilverShop\Extension\ShopConfigExtension 31 | 32 | SilverStripe\Assets\Image: 33 | extensions: 34 | - SilverShop\Extension\ProductImageExtension 35 | 36 | SilverStripe\Omnipay\Model\Payment: 37 | extensions: 38 | - SilverShop\Extension\PaymentExtension 39 | 40 | SilverStripe\Omnipay\Service\PaymentService: 41 | extensions: 42 | - SilverShop\Extension\PaymentServiceExtension 43 | 44 | SilverStripe\Security\Member: 45 | extensions: 46 | - SilverShop\Extension\MemberExtension 47 | -------------------------------------------------------------------------------- /src/Page/CartPageController.php: -------------------------------------------------------------------------------- 1 | getFailover && $this->getFailover()->Title) { 29 | return $this->getFailover()->Title; 30 | } 31 | return _t('SilverShop\Page\CartPage.DefaultTitle', 'Shopping Cart'); 32 | } 33 | 34 | /** 35 | * A form for updating cart items 36 | */ 37 | public function CartForm(): CartForm|bool 38 | { 39 | $cart = $this->Cart(); 40 | if (!$cart) { 41 | return false; 42 | } 43 | $cartForm = CartForm::create($this, 'CartForm', $cart); 44 | 45 | $this->extend('updateCartForm', $cartForm); 46 | 47 | return $cartForm; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/dist/css/packagingslip.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | text-align: left; 3 | border: 0; 4 | padding: 0; 5 | margin: 0; 6 | background: #e9e9e9; 7 | } 8 | 9 | body { 10 | font-size: 62.5%; 11 | } 12 | 13 | p.footertext { 14 | text-align: center; 15 | color: #666; 16 | font-size: 10px; 17 | } 18 | 19 | * { 20 | font-size: 1em; 21 | } 22 | 23 | h2.pageTitle { 24 | font-size: 2em; 25 | } 26 | 27 | #OrderInformation div, 28 | #OrderStatusForm, 29 | #OrderStatusLog { 30 | margin: 20px; 31 | background: #fff; 32 | padding: 20px; 33 | border: 1px solid #333; 34 | page-break-after: always; 35 | font-family: Arial, Helvetica, sans-serif; 36 | width: 600px; 37 | text-align: left; 38 | } 39 | 40 | .packingSlip { 41 | width: 98%; 42 | border-collapse: collapse; 43 | } 44 | 45 | .packingSlip th { 46 | color: #666; 47 | font-size: 1.8em; 48 | padding: 5px; 49 | } 50 | 51 | .packingSlip td { 52 | border: 1px solid #ccc; 53 | font-size: 1.2em; 54 | padding: 5px; 55 | } 56 | 57 | .packingSlip td h2 { 58 | color: #666; 59 | font-size: 2.5em; 60 | } 61 | 62 | .packingSlip tr.orderDetails { 63 | font-weight: bold; 64 | border-bottom: 10px; 65 | } 66 | 67 | .packingSlip.orderDetails { 68 | margin-top: 10px; 69 | } 70 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/ProductMenu.ss: -------------------------------------------------------------------------------- 1 |
2 |

<% with $Level(1) %>$Title<% end_with %>

3 | 22 |
23 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/ProductGroupPagination.ss: -------------------------------------------------------------------------------- 1 | <% if $Products.MoreThanOnePage %> 2 |
3 |

<%t SilverShop\Includes\ProductGroup.Page "Page" %>: 4 | <% if $Products.NotFirstPage %> 5 | 6 | <% end_if %> 7 | 8 | 9 | <% loop $Products.PaginationSummary(4) %> 10 | <% if $CurrentBool %> 11 | $PageNum 12 | <% else %> 13 | <% if $Link %> 14 | ">$PageNum 15 | <% else %> 16 | … 17 | <% end_if %> 18 | <% end_if %> 19 | <% end_loop %> 20 | 21 | 22 | <% if $Products.NotLastPage %> 23 | 24 | <% end_if %> 25 |

26 |
27 | <% end_if %> 28 | -------------------------------------------------------------------------------- /src/Checkout/SinglePageCheckoutComponentConfig.php: -------------------------------------------------------------------------------- 1 | addComponent(CustomerDetails::create()); 22 | $this->addComponent(ShippingAddress::create()); 23 | $this->addComponent(BillingAddress::create()); 24 | if (Checkout::member_creation_enabled() && !Security::getCurrentUser()) { 25 | $this->addComponent(Membership::create()); 26 | } 27 | if (count(GatewayInfo::getSupportedGateways()) > 1) { 28 | $this->addComponent(Payment::create()); 29 | } 30 | $this->addComponent(Notes::create()); 31 | $this->addComponent(Terms::create()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Model/Buyable.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CheckoutStep extends Extension 15 | { 16 | use Configurable; 17 | 18 | /** 19 | * Get the next step action 20 | */ 21 | private function nextstep(): ?string 22 | { 23 | $steps = $this->owner->getSteps(); 24 | $found = false; 25 | foreach ($steps as $step => $class) { 26 | //determine if this is the current step 27 | if (method_exists($this, $step)) { 28 | $found = true; 29 | } elseif ($found) { 30 | return $step; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | public function NextStepLink($nextstep = null): string 37 | { 38 | if (!$nextstep) { 39 | $nextstep = $this->nextstep(); 40 | } 41 | $anchor = Config::inst()->get(SteppedCheckoutExtension::class, 'continue_anchor'); 42 | $anchor = $anchor ? '#' . $anchor : ''; 43 | return $this->owner->Link($nextstep) . $anchor; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Checkout/Step/Summary.php: -------------------------------------------------------------------------------- 1 | ConfirmationForm(); 21 | return [ 22 | 'OrderForm' => $paymentForm, 23 | ]; 24 | } 25 | 26 | public function ConfirmationForm(): PaymentForm 27 | { 28 | $checkoutComponentConfig = CheckoutComponentConfig::create(ShoppingCart::curr(), false); 29 | $checkoutComponentConfig->addComponent(Notes::create()); 30 | $checkoutComponentConfig->addComponent(Terms::create()); 31 | $this->owner->extend('updateConfirmationComponentConfig', $checkoutComponentConfig); 32 | 33 | $paymentForm = PaymentForm::create($this->owner, 'ConfirmationForm', $checkoutComponentConfig); 34 | $paymentForm->setFailureLink($this->owner->Link('summary')); 35 | $this->owner->extend('updateConfirmationForm', $paymentForm); 36 | 37 | return $paymentForm; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Reports/AbandonedCartReport.php: -------------------------------------------------------------------------------- 1 | $period, 32 | 'Count' => 'Count', 33 | 'TotalValue' => 'Total Value', 34 | ]; 35 | } 36 | 37 | public function query($params): ShopReportQuery|SQLSelect 38 | { 39 | return parent::query($params) 40 | ->selectField('COUNT("SilverShop_Order"."ID")', 'Count') 41 | ->selectField('SUM("SilverShop_Order"."Total")', 'TotalValue') 42 | ->addWhere(['"Status" = ?' => 'Cart']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Reports/SideReport/FeaturedProducts.php: -------------------------------------------------------------------------------- 1 | filter('Featured', 1)->sort('Title'); 38 | } 39 | 40 | public function columns(): array 41 | { 42 | return [ 43 | 'Title' => [ 44 | 'title' => 'Title', 45 | 'link' => true, 46 | ], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Reports/ShopSalesReport.php: -------------------------------------------------------------------------------- 1 | $period, 34 | 'Count' => 'Order Count', 35 | 'Sales' => 'Total Sales', 36 | ]; 37 | } 38 | 39 | public function query($params): ShopReportQuery|SQLSelect 40 | { 41 | return parent::query($params) 42 | ->selectField('COUNT("SilverShop_Order"."ID")', 'Count') 43 | ->selectField('SUM("SilverShop_Order"."Total")', 'Sales'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/php/Model/Product/CustomProduct.php: -------------------------------------------------------------------------------- 1 | 'Varchar', 16 | 'Price' => 'Currency', 17 | ]; 18 | 19 | private static string $order_item = CustomProduct_OrderItem::class; 20 | 21 | private static string $table_name = 'SilverShop_Test_CustomProduct'; 22 | 23 | public function createItem(int $quantity = 1, array $filter = []): OrderItem 24 | { 25 | $itemClass = self::config()->get('order_item'); 26 | 27 | if (!$itemClass) { 28 | $itemClass = CustomProduct_OrderItem::class; 29 | } 30 | 31 | $item = Injector::inst()->create($itemClass); 32 | $item->CustomProductID = $this->ID; 33 | 34 | if ($filter !== []) { 35 | $item->update($filter); 36 | } 37 | 38 | return $item; 39 | } 40 | 41 | public function canPurchase(?Member $member = null, int $quantity = 1): bool 42 | { 43 | return $this->Price > 0; 44 | } 45 | 46 | public function sellingPrice(): float 47 | { 48 | return $this->Price; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/php/Cart/ShoppingCartTest_TestShoppingCartHooksExtension.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ShoppingCartTest_TestShoppingCartHooksExtension extends Extension implements TestOnly 12 | { 13 | public static $stack = []; 14 | 15 | public static function reset(): void 16 | { 17 | self::$stack = []; 18 | } 19 | 20 | public function onStartOrder(): void 21 | { 22 | self::$stack[] = 'onStartOrder'; 23 | } 24 | 25 | public function beforeAdd($buyable, $quantity, $filter): void 26 | { 27 | self::$stack[] = 'beforeAdd'; 28 | } 29 | 30 | public function afterAdd($item, $buyable, $quantity, $filter): void 31 | { 32 | self::$stack[] = 'afterAdd'; 33 | } 34 | 35 | public function beforeRemove($buyable, $quantity, $filter): void 36 | { 37 | self::$stack[] = 'beforeRemove'; 38 | } 39 | 40 | public function afterRemove($buyable, $quantity, $filter): void 41 | { 42 | self::$stack[] = 'afterRemove'; 43 | } 44 | 45 | public function beforeSetQuantity($buyable, $quantity, $filter): void 46 | { 47 | self::$stack[] = 'beforeSetQuantity'; 48 | } 49 | 50 | public function afterSetQuantity($item, $buyable, $quantity, $filter): void 51 | { 52 | self::$stack[] = 'afterSetQuantity'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/php/ORM/Filters/MatchObjectFilterTest.php: -------------------------------------------------------------------------------- 1 | 5], ['ProductID']); 15 | $this->assertEquals($matchObjectFilter->getFilter(), ['"ProductID" = \'5\''], 'ID was added to filter'); 16 | } 17 | 18 | public function testMissingValues(): void 19 | { 20 | // Tests that missing values are included in the filter as IS NULL or = 0 21 | // Missing value for a has_one relationship field. 22 | $filter = new MatchObjectFilter(OrderItem::class, [], ['ProductID']); 23 | $this->assertEquals( 24 | $filter->getFilter(), 25 | ['"ProductID" IS NULL'], 26 | 'missing ID value became IS NULL' 27 | ); 28 | // Missing value for a db field. 29 | $filter = new MatchObjectFilter(OrderItem::class, [], ['ProductVersion']); 30 | $this->assertEquals( 31 | $filter->getFilter(), 32 | ['"ProductVersion" IS NULL'], 33 | 'missing DB value became IS NULL' 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Tax/FlatTax.php: -------------------------------------------------------------------------------- 1 | Type = self::config()->exclusive ? 'Chargable' : 'Ignored'; 35 | } 36 | 37 | /** 38 | * Get the tax amount to charge on the order. 39 | */ 40 | public function value($incoming): int|float 41 | { 42 | $this->Rate = self::config()->rate; 43 | //inclusive tax requires a different calculation 44 | return self::config()->exclusive 45 | ? 46 | $incoming * $this->Rate 47 | : 48 | $incoming - round($incoming / (1 + $this->Rate), Order::config()->rounding_precision); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_Payments.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% loop $Payments %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% if $ShowMessages %> 22 | <% loop $Messages %> 23 | 24 | 27 | 28 | <% end_loop %> 29 | <% end_if %> 30 | <% end_loop %> 31 | 32 |
<%t SilverShop\Payment.PaymentsHeadline "Payment(s)" %>
<%t SilverStripe\Omnipay\Model\Payment.Date "Date" %><%t SilverStripe\Omnipay\Model\Payment.Amount "Amount" %><%t SilverStripe\Omnipay\Model\Payment.db_Status "Payment Status" %><%t SilverStripe\Omnipay\Model\Payment.db_Gateway "Method" %>
$Created.Nice$Amount.Nice $Currency$PaymentStatus$GatewayTitle
25 | $ClassName $Message $User.Name 26 |
33 | -------------------------------------------------------------------------------- /tests/php/Fixtures/Orders.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Page\Product: 2 | laptop: 3 | Title: Laptop 4 | BasePrice: 1940.00 5 | AllowPurchase: 1 6 | Width: 50 7 | Height: 30 8 | Depth: 10 9 | Weight: 3.4 10 | bag: 11 | Title: Satchel Bag 12 | BasePrice: 30 13 | AllowPurchase: 1 14 | Width: 40 15 | Height: 60 16 | Depth: 4 17 | Weight: 0.8 18 | battery: 19 | Title: Battery 20 | BasePrice: 5 21 | Width: 1 22 | Height: 6 23 | Depth: 1 24 | Weight: 0.06 25 | 26 | SilverShop\Model\Address: 27 | address1: 28 | Address: '201-203 BROADWAY AVE' 29 | Address2: 'U 235' 30 | City: 'WEST BEACH' 31 | State: South Australia 32 | PostCode: 5024 33 | Country: AU 34 | address2: 35 | Address: '2 Foobar Ave' 36 | City: Lorem 37 | State: Ipsum 38 | PostCode: 1234 39 | Country: NZ 40 | SilverShop\Model\Order: 41 | #$2000 order 42 | cart1: 43 | Name: Blah 44 | Status: Cart 45 | ShippingAddress: =>SilverShop\Model\Address.address1 46 | BillingAddress: =>SilverShop\Model\Address.address1 47 | 48 | SilverShop\Model\Product\OrderItem: 49 | item1: 50 | Order: =>SilverShop\Model\Order.cart1 51 | Product: =>SilverShop\Page\Product.laptop 52 | Quantity: 1 53 | item2: 54 | Order: =>SilverShop\Model\Order.cart1 55 | Product: =>SilverShop\Page\Product.bag 56 | Quantity: 1 57 | item3: 58 | Order: =>SilverShop\Model\Order.cart1 59 | Product: =>SilverShop\Page\Product.battery 60 | Quantity: 6 61 | -------------------------------------------------------------------------------- /tests/php/Admin/test_products.csv: -------------------------------------------------------------------------------- 1 | Product ID,VariationID,Title,Long Description,Price,Stock,Category,Colour,Size,Weight,Photo,Variation,Variation1,Variation2,Variation3,Variation4,Variation5,Variation6 2 | 123,,Beach Ball,"A beach ball with a difference. This ball is made from the finest plastic in the world. 3 | Hours of fun are to be had with the whole family down at the beach with this ball.",,,Sports,,,0.2,beachball.jpg,,,,,,, 4 | 123,123-1,Beach Ball,,5,1,,Blue,1,,,,,Size:1,Colour:Blue,,, 5 | 123,123-2,Beach Ball,,5,3,,Red,2,,,,,Size:2,Colour:Red,,, 6 | 123,123-3,Beach Ball,,6,3,,Blue,2,,,,,Size:2,Colour:Blue,,, 7 | 124,,Socks,The comfiest pair of socks you'll ever own.,12,12,Apparel,"Black,White","8,9,10",0.2,socks.png,,,"Size:8,9,10","Colour:Black,White",,, 8 | 125,,100% Cotton T-Shirt,Finest t-shirt you'll ever own,25,1,Apparel,"White,Red","S,M,L,XL",0.5,tshirt.jpg,"Gender:Male, Female",,"Size:S,M,L,XL","Colour:White,Red",,, 9 | CAR001,,Car,An actual car,25000,15,Automotive,,,3000,car.jpg,,,,,,, 10 | 23140402,,Mp3 Player,Listen to your music on the go with this Mp3 player. Plays Mp3 files to your headphones.,,,Electronics,,,0.6,mp3player.jpg,,,,,,, 11 | ,23140402-1,Mp3 Player,,100,0,,Silver,1GB,,,,,Size:1GB,Colour:Silver,,, 12 | ,23140402-2,Mp3 Player,,150,3,,Silver,2GB,,,,,Size:2GB,Colour:Silver,,, 13 | ,23140402-3,Mp3 Player,,100,6,,Blue,1GB,,,,,Size:1GB,Colour:Blue,,, 14 | ,23140402-4,Mp3 Player,,150,1,,Blue,2GB,,,,,Size:2GB,Colour:Blue,,, 15 | ,23140402-5,Mp3 Player,,400,9,,Silver,8GB,,,,,Size:8GB,Colour:Silver,,, 16 | -------------------------------------------------------------------------------- /tests/php/Forms/AddProductFormTest.php: -------------------------------------------------------------------------------- 1 | objFromFixture(Product::class, "socks")); 18 | $addProductForm = AddProductForm::create($controller); 19 | $addProductForm->setMaximumQuantity(10); 20 | 21 | $addProductForm->addtocart( 22 | [ 23 | 'Quantity' => 11, 24 | ], 25 | $addProductForm 26 | ); 27 | $order = ShoppingCart::curr(); 28 | $this->assertEquals( 29 | 10, 30 | $order->Items()->First()->Quantity, 31 | 'Quantity set to maximum of 10 when over the maximum quantity' 32 | ); 33 | 34 | ShoppingCart::singleton()->clear(); 35 | $addProductForm->addtocart( 36 | [ 37 | 'Quantity' => 4, 38 | ], 39 | $addProductForm 40 | ); 41 | $order = ShoppingCart::curr(); 42 | $this->assertEquals( 43 | 4, 44 | $order->Items()->First()->Quantity, 45 | 'Quantity should be 4' 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/php/Admin/ProductBulkLoaderTest.php: -------------------------------------------------------------------------------- 1 | load($filepath); 26 | 27 | // Test that right amount of columns was imported 28 | $this->assertEquals(13, $results->Count(), 'Test correct count of imported data'); 29 | 30 | // Test that columns were correctly imported 31 | $obj = Product::get()->filter('Title', 'Socks')->first(); 32 | $this->assertNotNull($obj, "New product exists"); 33 | $this->assertEquals("

The comfiest pair of socks you'll ever own.

", $obj->Content, "Content matches"); 34 | $this->assertEquals(12, $obj->BasePrice, "Checking price matches."); 35 | $this->assertEquals(124, $obj->InternalItemID, "Checking ID matches"); 36 | fclose($file); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tasks/PopulateCartTask.php: -------------------------------------------------------------------------------- 1 | getVar('count') ? $request->getVar('count') : 5; 26 | if ($products = Versioned::get_by_stage(Product::class, 'Live', '', 'RAND()', '', $count)) { 27 | foreach ($products as $product) { 28 | $variations = $product->Variations(); 29 | if ($variations->exists()) { 30 | $product = $variations->sort('RAND()')->first(); 31 | } 32 | $quantity = rand(1, 5); 33 | if ($product->canPurchase(Security::getCurrentUser(), $quantity)) { 34 | $shoppingCart->add($product, $quantity); 35 | } 36 | } 37 | } 38 | Controller::curr()->redirect(CheckoutPage::find_link()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ORM/FieldType/I18nDatetime.php: -------------------------------------------------------------------------------- 1 | value) { 19 | return null; 20 | } 21 | return date( 22 | _t('SilverShop\Generic.DateTimeFormatNice', 'm/d/Y h:i A'), 23 | $this->getTimestamp() 24 | ); 25 | } 26 | 27 | /** 28 | * Returns the date in the format given in the lang file. 29 | * 'SilverShop\Generic.DateFormatNice'. Defaults to 'm/d/Y' 30 | */ 31 | public function NiceDate(): ?string 32 | { 33 | if (!$this->value) { 34 | return null; 35 | } 36 | return date( 37 | _t('SilverShop\Generic.DateFormatNice', 'm/d/Y'), 38 | $this->getTimestamp() 39 | ); 40 | } 41 | 42 | /** 43 | * Returns the 24h datetime in the format given in the lang file. 44 | * 'SilverShop\Generic.DateTimeFormatNice24'. Defaults to 'd/m/Y H:i' 45 | */ 46 | public function Nice24(): ?string 47 | { 48 | if (!$this->value) { 49 | return null; 50 | } 51 | return date( 52 | _t('SilverShop\Generic.DateTimeFormatNice24', 'd/m/Y H:i'), 53 | $this->getTimestamp() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/php/Fixtures/Carts.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Page\ProductCategory: 2 | kites: 3 | Title: Kites 4 | URLSegment: kites 5 | 6 | SilverShop\Model\Variation\AttributeType: 7 | kitesize: 8 | Name: Kite Size 9 | Label: Size 10 | color: 11 | Name: Color 12 | Label: Color 13 | 14 | SilverShop\Model\Variation\AttributeValue: 15 | kitesize_small: 16 | Value: Small 17 | Type: =>SilverShop\Model\Variation\AttributeType.kitesize 18 | color_red: 19 | Value: Red 20 | Type: =>SilverShop\Model\Variation\AttributeType.color 21 | 22 | SilverShop\Page\Product: 23 | extremekite2000: 24 | Title: Extereme Kite 2000 25 | BasePrice: 35.00 26 | URLSegment: extreme-kite-2000 27 | AllowPurchase: 1 28 | Weight: 0.3 29 | Parent: =>SilverShop\Page\ProductCategory.kites 30 | VariationAttributeTypes: =>SilverShop\Model\Variation\AttributeType.kitesize,=>SilverShop\Model\Variation\AttributeType.color 31 | 32 | SilverShop\Model\Variation\Variation: 33 | extremekite2000_redsmall: 34 | InternalItemID: 123 35 | Price: 35.00 36 | Product: =>SilverShop\Page\Product.extremekite2000 37 | AttributeValues: =>SilverShop\Model\Variation\AttributeValue.kitesize_small,=>SilverShop\Model\Variation\AttributeValue.color_red 38 | 39 | SilverShop\Model\Order: 40 | kitecart: 41 | Name: Cart with small red kite 42 | 43 | SilverShop\Model\Variation\OrderItem: 44 | vcart_extremekite2000_redsmall: 45 | Product: =>SilverShop\Page\Product.extremekite2000 46 | ProductVariation: =>SilverShop\Model\Variation\Variation.extremekite2000_redsmall 47 | Quantity: 1 48 | Order: =>SilverShop\Model\Order.kitecart 49 | -------------------------------------------------------------------------------- /_config/legacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: silvershop-core-legacy 3 | --- 4 | # Mapping rules to map old model-classes to new ones. 5 | SilverStripe\ORM\DatabaseAdmin: 6 | classname_value_remapping: 7 | AccountPage: SilverShop\Page\AccountPage 8 | CartPage: SilverShop\Page\CartPage 9 | CheckoutPage: SilverShop\Page\CheckoutPage 10 | Address: SilverShop\Model\Address 11 | Order: SilverShop\Model\Order 12 | OrderAttribute: SilverShop\Model\OrderAttribute 13 | OrderItem: SilverShop\Model\OrderItem 14 | OrderModifier: SilverShop\Model\Modifiers\OrderModifier 15 | OrderStatusLog: SilverShop\Model\OrderStatusLog 16 | FreeShippingModifier: SilverShop\Model\Modifiers\Shipping\Free 17 | PickupShippingModifier: SilverShop\Model\Modifiers\Shipping\Pickup 18 | ShippingModifier: SilverShop\Model\Modifiers\Shipping\Base 19 | SimpleShippingModifier: SilverShop\Model\Modifiers\Shipping\Simple 20 | WeightShippingModifier: SilverShop\Model\Modifiers\Shipping\Weight 21 | SubTotalModifier: SilverShop\Model\Modifiers\SubTotal 22 | FlatTaxModifier: SilverShop\Model\Modifiers\Tax\FlatTax 23 | GlobalTaxModifier: SilverShop\Model\Modifiers\Tax\GlobalTax 24 | TaxModifier: SilverShop\Model\Modifiers\Tax\Base 25 | Product: SilverShop\Page\Product 26 | Product_OrderItem: SilverShop\Model\Product\OrderItem 27 | ProductCategory: SilverShop\Page\ProductCategory 28 | ProductAttributeType: SilverShop\Model\Variation\AttributeType 29 | ProductAttributeValue: SilverShop\Model\Variation\AttributeValue 30 | ProductVariation: SilverShop\Model\Variation\Variation 31 | ProductVariation_OrderItem: SilverShop\Model\Variation\OrderItem 32 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_AdminNotificationEmail.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%t SilverShop\ShopEmail.AdminNotificationTitle "Shop Receipt" %> 6 | <% include SilverShop\Includes\OrderReceiptStyle %> 7 | 8 | 9 | 10 | 11 | 33 | 34 |
12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | <% if $Order %> 22 | <% loop $Order %> 23 | 24 | 27 | 28 | <% end_loop %> 29 | <% end_if %> 30 | 31 |
16 |

$Subject

17 |
25 | <% include SilverShop\Model\Order %> 26 |
32 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/php/Model/Variation/VariationVersionTest.php: -------------------------------------------------------------------------------- 1 | ball = $this->objFromFixture(Product::class, "ball"); 32 | $this->mp3player = $this->objFromFixture(Product::class, "mp3player"); 33 | $this->redLarge = $this->objFromFixture(Variation::class, "redLarge"); 34 | } 35 | 36 | public function testVariationsPersistOnUnpublish(): void 37 | { 38 | $attributeType = $this->objFromFixture(AttributeType::class, "color"); 39 | $values = ['Black', 'Blue']; 40 | 41 | $this->mp3player->generateVariationsFromAttributes($attributeType, $values); 42 | $this->mp3player->publishRecursive(); 43 | 44 | $hasManyList = $this->mp3player->Variations(); 45 | $this->assertEquals($hasManyList->Count(), 2, "two variations created and persist after product unpublished"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ORM/OrderItemList.php: -------------------------------------------------------------------------------- 1 | Sum('Quantity'); 15 | } 16 | 17 | public function Plural(): bool 18 | { 19 | return $this->Quantity() > 1; 20 | } 21 | 22 | /** 23 | * Sums up all of desired field for items, and multiply by quantity. 24 | * Optionally sum product field instead. 25 | * 26 | * @param string $field - field to sum 27 | * @param boolean $onproduct - sum from product or not 28 | * 29 | * @return float sum total of field 30 | */ 31 | public function Sum($field, $onproduct = false): float 32 | { 33 | $total = 0; 34 | foreach ($this->getIterator() as $item) { 35 | $quantity = ($field === 'Quantity') ? 1 : $item->Quantity; 36 | if (!$onproduct) { 37 | $total += $item->$field * $quantity; 38 | } elseif ($item->hasMethod($field)) { 39 | $total += $item->$field() * $quantity; 40 | } elseif ($product = $item->Product()) { 41 | $total += $product->$field * $quantity; 42 | } 43 | } 44 | return $total; 45 | } 46 | 47 | /** 48 | * Add up the totals of all the order items in this list. 49 | */ 50 | public function SubTotal(): int|float 51 | { 52 | $result = 0; 53 | foreach ($this->getIterator() as $item) { 54 | $result += $item->Total(); 55 | } 56 | 57 | return $result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Shipping/Simple.php: -------------------------------------------------------------------------------- 1 | Country(); 24 | if ($country && isset(self::config()->charges_by_country[$country])) { 25 | return self::config()->charges_by_country[$country]; 26 | } 27 | 28 | return self::config()->default_charge; 29 | } 30 | 31 | public function getTableTitle(): string 32 | { 33 | if ($country = $this->Country()) { 34 | $countryList = SiteConfig::current_site_config()->getCountriesList(); 35 | 36 | return _t( 37 | __CLASS__ . '.ShipToCountry', 38 | 'Ship to {Country}', 39 | '', 40 | ['Country' => $countryList[$country]] 41 | ); 42 | } 43 | return parent::getTableTitle(); 44 | } 45 | 46 | /** 47 | * @return ?string 48 | */ 49 | public function Country() 50 | { 51 | if (!($order = $this->Order())) { 52 | return null; 53 | } 54 | if ($order->getShippingAddress()->exists() && $order->getShippingAddress()->Country) { 55 | return $order->getShippingAddress()->Country; 56 | } 57 | 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Reports/TaxReport.php: -------------------------------------------------------------------------------- 1 | $period, 32 | 'Count' => 'Order Count', 33 | 'Sales' => 'Total Sales', 34 | 'Tax' => 'Total Tax', 35 | ]; 36 | } 37 | 38 | public function query($params): ShopReportQuery|SQLSelect 39 | { 40 | return parent::query($params) 41 | ->addInnerJoin( 42 | 'SilverShop_OrderAttribute', 43 | '"SilverShop_OrderAttribute"."OrderID" = "SilverShop_Order"."ID" AND "SilverShop_OrderAttribute"."ClassName" LIKE \'%TaxModifier\'' 44 | ) 45 | ->addInnerJoin('SilverShop_OrderModifier', '"SilverShop_OrderModifier"."ID" = "SilverShop_OrderAttribute"."ID"') 46 | ->selectField('COUNT("SilverShop_Order"."ID")', 'Count') 47 | ->selectField('SUM("SilverShop_OrderModifier"."Amount")', 'Tax') 48 | ->selectField('SUM("SilverShop_Order"."Total")', 'Sales'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Forms/SetLocationForm.php: -------------------------------------------------------------------------------- 1 | getCountriesList(); 20 | $fieldList = FieldList::create( 21 | $countryfield = DropdownField::create("Country", _t(__CLASS__ . '.Country', 'Country'), $countries) 22 | ); 23 | $countryfield->setHasEmptyDefault(true); 24 | $countryfield->setEmptyString(_t(__CLASS__ . '.ChooseCountry', 'Choose country...')); 25 | $actions = FieldList::create( 26 | FormAction::create("setLocation", "set") 27 | ->setUseButtonTag(Config::inst()->get(ShopConfigExtension::class, 'forms_use_button_tag')) 28 | ); 29 | parent::__construct($requestHandler, $name, $fieldList, $actions); 30 | //load currently set location 31 | if ($location = singleton(ShopUserInfo::class)->getLocation()) { 32 | $countryfield->setHasEmptyDefault(false); 33 | $this->loadDataFrom($location); 34 | } 35 | } 36 | 37 | public function setLocation(array $data, Form $form): void 38 | { 39 | singleton(ShopUserInfo::class)->setLocation($data); 40 | $this->controller->redirectBack(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ShopUserInfo.php: -------------------------------------------------------------------------------- 1 | getLocationData(); 23 | } 24 | 25 | public function setLocation(array $location): static 26 | { 27 | $this->setLocationData($location); 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Get location of user 34 | */ 35 | public function getAddress(): ?Address 36 | { 37 | $address = null; 38 | if (($data = $this->getLocationData()) !== []) { 39 | $address = Address::create(); 40 | $address->update($data); 41 | $address->ID = 0; //ensure not in db 42 | } 43 | 44 | return $address; 45 | } 46 | 47 | /** 48 | * Set location of user 49 | * 50 | * @param Address $address location 51 | * @return $this 52 | */ 53 | public function setAddress(Address $address): static 54 | { 55 | $this->setLocationData($address->toMap()); 56 | 57 | return $this; 58 | } 59 | 60 | protected function getLocationData(): array 61 | { 62 | $data = ShopTools::getSession()->get('UserInfo.Location'); 63 | return is_array($data) ? $data : []; 64 | } 65 | 66 | protected function setLocationData(array $data): void 67 | { 68 | ShopTools::getSession()->set('UserInfo.Location', $data); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /templates/SilverShop/Dev/ShopDevelopmentAdmin.ss: -------------------------------------------------------------------------------- 1 |

<%t SilverShop\Dev\ShopDevelopmentAdmin.CartTasks "Cart tasks" %>

2 | 7 | 8 |

<%t SilverShop\Dev\ShopDevelopmentAdmin.BuildTasks "Build Tasks" %>

9 | 17 | 18 |

<%t SilverShop\Dev\ShopDevelopmentAdmin.UnitTests "Unit Tests" %>

19 | 22 | -------------------------------------------------------------------------------- /src/Forms/OrderActionsFormValidator.php: -------------------------------------------------------------------------------- 1 | required = $fieldFactory->getFieldName( 21 | array_merge( 22 | $this->required, 23 | array_intersect( 24 | [ 25 | 'type', 26 | 'name', 27 | 'number', 28 | 'startMonth', 29 | 'startYear', 30 | 'expiryMonth', 31 | 'expiryYear', 32 | 'cvv', 33 | 'issueNumber' 34 | ], 35 | GatewayInfo::requiredFields($gateway) 36 | ) 37 | ) 38 | ); 39 | } 40 | } 41 | 42 | return parent::php($data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tasks/CartCleanupTask.php: -------------------------------------------------------------------------------- 1 | config()->get('delete_after_mins')) { 35 | throw new LogicException('No valid time specified in "delete_after_mins"'); 36 | } 37 | 38 | $count = 0; 39 | $time = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() - $this->config()->get('delete_after_mins') * 60); 40 | 41 | $this->log('Deleting all orders since ' . $time); 42 | 43 | $dataList = Order::get()->filter( 44 | [ 45 | 'Status' => 'Cart', 46 | 'LastEdited:LessThan' => $time, 47 | ] 48 | ); 49 | foreach ($dataList as $order) { 50 | $this->log(sprintf('Deleting order #%s (Reference: %s)', $order->ID, $order->Reference)); 51 | $order->delete(); 52 | $order->destroy(); 53 | $count++; 54 | } 55 | 56 | $this->log("$count old carts removed."); 57 | } 58 | 59 | protected function log(string $msg) 60 | { 61 | echo $msg . "\n"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Model/Variation/AttributeValue.php: -------------------------------------------------------------------------------- 1 | ProductVariation() 18 | * @property int $TypeID 19 | */ 20 | class AttributeValue extends DataObject 21 | { 22 | private static array $db = [ 23 | 'Value' => 'Varchar', 24 | 'Sort' => 'Int', 25 | ]; 26 | 27 | private static array $has_one = [ 28 | 'Type' => AttributeType::class, 29 | ]; 30 | 31 | private static array $belongs_many_many = [ 32 | 'ProductVariation' => Variation::class, 33 | ]; 34 | 35 | private static array $summary_fields = [ 36 | 'Value' => 'Value', 37 | ]; 38 | 39 | private static array $indexes = [ 40 | 'LastEdited' => true, 41 | 'Sort' => true, 42 | ]; 43 | 44 | private static string $table_name = 'SilverShop_AttributeValue'; 45 | 46 | private static string $default_sort = '"TypeID" ASC, "Sort" ASC, "Value" ASC'; 47 | 48 | private static string $singular_name = 'Value'; 49 | 50 | private static string $plural_name = 'Values'; 51 | 52 | public function getCMSFields(): FieldList 53 | { 54 | $this->beforeUpdateCMSFields( 55 | function (FieldList $fieldList): void { 56 | 57 | $fieldList->removeByName('TypeID'); 58 | $fieldList->removeByName('Sort'); 59 | } 60 | ); 61 | 62 | return parent::getCMSFields(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/ProductGroupItem.ss: -------------------------------------------------------------------------------- 1 |
2 | <% if $Image %> 3 | "> 4 | <%t SilverShop\Page\Product.ImageAltText " /> 5 | 6 | <% else %> 7 | " class="noimage"> 8 | <% end_if %> 9 |

">$Title

10 | <% if $Model %>

<%t SilverShop\Page\Product.Model "Model" %>: $Model.XML

<% end_if %> 11 |
12 | <% include SilverShop\Includes\Price %> 13 | <% if $View %> 14 | 19 | <% else %> 20 | <% if $canPurchase %> 21 | 29 | <% end_if %> 30 | <% end_if %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/Extension/PaymentExtension.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class PaymentExtension extends Extension 18 | { 19 | private static array $has_one = [ 20 | 'Order' => Order::class, 21 | ]; 22 | 23 | public function onAwaitingAuthorized(ServiceResponse $response): void 24 | { 25 | $this->placeOrder(); 26 | } 27 | 28 | public function onAwaitingCaptured(ServiceResponse $response): void 29 | { 30 | $this->placeOrder(); 31 | } 32 | 33 | public function onAuthorized(ServiceResponse $response): void 34 | { 35 | $this->placeOrder(); 36 | } 37 | 38 | public function onCaptured(ServiceResponse $response): void 39 | { 40 | // ensure order is being reloaded from DB, to prevent dealing with stale data! 41 | /** 42 | * @var Order $order 43 | */ 44 | $order = Order::get()->byID($this->owner->OrderID); 45 | if ($order && $order->exists()) { 46 | OrderProcessor::create($order)->completePayment(); 47 | } 48 | } 49 | 50 | protected function placeOrder(): void 51 | { 52 | // ensure order is being reloaded from DB, to prevent dealing with stale data! 53 | /** 54 | * @var Order $order 55 | */ 56 | $order = Order::get()->byID($this->owner->OrderID); 57 | if ($order && $order->exists()) { 58 | OrderProcessor::create($order)->placeOrder(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/dist/javascript/CheckoutPage.nojquery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Addressbook checkout component 3 | * This handles a dropdown or radio buttons containing existing addresses or payment methods, 4 | * with one of the options being "create a new ____". When that last option is selected, the 5 | * other fields need to be shown, otherwise they need to be hidden. 6 | */ 7 | function onExistingValueChange() { 8 | let existingValues = document.querySelectorAll('.hasExistingValues'); 9 | if(!existingValues) return; 10 | 11 | existingValues.forEach(function (container, idx) { 12 | let toggle = document.querySelector('.existingValues select, .existingValues input:checked'); 13 | 14 | // visible if the value is not an ID (numeric) 15 | let toggleState = Number.isNaN(parseInt(toggle.value)); 16 | let toggleFields = container.querySelectorAll(".field:not(.existingValues)"); 17 | 18 | // animate the fields - hide or show 19 | if (toggleFields && toggleFields.length > 0) { 20 | toggleFields.forEach(field => { 21 | field.style.display = toggleState ? '' : 'none'; 22 | }) 23 | } 24 | 25 | // clear them out 26 | toggleFields.forEach(field => { 27 | field.querySelectorAll('input, select, textarea').forEach(f => { 28 | f.value = ''; 29 | f.disabled = toggleState ? '' : 'disabled'; 30 | }); 31 | }); 32 | }); 33 | } 34 | 35 | let selectors = document.querySelectorAll('.existingValues select'); 36 | if(selectors) selectors.forEach(selector => selector.addEventListener('change', onExistingValueChange)); 37 | 38 | let inputs = document.querySelectorAll('.existingValues input[type=radio]') 39 | if(inputs) inputs.forEach(input => input.addEventListener('click', onExistingValueChange)); 40 | 41 | onExistingValueChange(); // handle initial state 42 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_ReceiptEmail.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%t SilverShop\ShopEmail.ReceiptTitle "Shop Receipt" %> 6 | <% include SilverShop\Includes\OrderReceiptStyle %> 7 | 8 | 9 | 10 | 11 | 38 | 39 |
12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | <% if $Order %> 27 | <% loop $Order %> 28 | 29 | 32 | 33 | <% end_loop %> 34 | <% end_if %> 35 | 36 |
16 |

$Subject

17 |
23 | $PurchaseCompleteMessage 24 |
30 | <% include SilverShop\Model\Order %> 31 |
37 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeSniffer ruleset for SilverStripe coding conventions. 4 | 5 | src 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | /thirdparty/* 34 | 35 | 36 | 37 | 38 | 39 | 40 | */SSTemplateParser.php$ 41 | */_fakewebroot/* 42 | */fixtures/* 43 | 44 | -------------------------------------------------------------------------------- /templates/SilverShop/Model/Order_ConfirmationEmail.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%t SilverShop\ShopEmail.ConfirmationTitle "Order Confirmation" %> 6 | <% include SilverShop\Includes\OrderReceiptStyle %> 7 | 8 | 9 | 10 | 11 | 38 | 39 |
12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | <% if $Order %> 27 | <% loop $Order %> 28 | 29 | 32 | 33 | <% end_loop %> 34 | <% end_if %> 35 | 36 |
16 |

$Subject

17 |
23 | $PurchaseCompleteMessage 24 |
30 | <% include SilverShop\Model\Order %> 31 |
37 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/SilverShop/Includes/AccountNavigation.ss: -------------------------------------------------------------------------------- 1 |
2 | 27 |
28 | <% with $CurrentMember %> 29 |
30 |
<%t SilverShop\Page\AccountPage.MemberName 'Name' %>
31 |
$Name
32 | 33 |
<%t SilverShop\Page\AccountPage.MemberEmail 'Email' %>
34 |
$Email
35 | 36 |
<%t SilverShop\Page\AccountPage.MemberSince 'Member Since' %>
37 |
$Created.Nice
38 | 39 |
<%t SilverShop\Page\AccountPage.NumberOfOrders 'Number of orders' %>
40 |
<% if $PastOrders %>{$PastOrders.Count}<% else %>0<% end_if %>
41 |
42 | <% end_with %> 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/Dev/ShopDevelopmentAdmin.php: -------------------------------------------------------------------------------- 1 | true 23 | ]; 24 | 25 | public function init() 26 | { 27 | parent::init(); 28 | 29 | // We allow access to this controller regardless of live-status or ADMIN permission only 30 | // if on CLI or with the database not ready. The latter makes it less errorprone to do an 31 | // initial schema build without requiring a default-admin login. 32 | // Access to this controller is always allowed in "dev-mode", or of the user is ADMIN. 33 | $canAccess = ( 34 | Director::isDev() 35 | || !Security::database_is_ready() 36 | // We need to ensure that DevelopmentAdminTest can simulate permission failures when running 37 | // "dev/tests" from CLI. 38 | || (Director::is_cli() && Director::isTest()) 39 | || Permission::check('ADMIN') 40 | ); 41 | if (!$canAccess) { 42 | return Security::permissionFailure( 43 | $this, 44 | 'This page is secured and you need administrator rights to access it. ' . 45 | 'Enter your credentials below and we will send you right along.' 46 | ); 47 | } 48 | 49 | //render the debug view 50 | $debugView = DebugView::create(); 51 | $debugView->renderHeader(); 52 | $debugView->renderInfo(_t('SilverShop\Generic.DevToolsTitle', 'Shop Development Tools'), Director::absoluteBaseURL()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Forms/CheckoutComponentValidator.php: -------------------------------------------------------------------------------- 1 | config = $checkoutComponentConfig; 20 | parent::__construct($this->config->getRequiredFields()); 21 | } 22 | 23 | public function php($data): bool 24 | { 25 | $valid = parent::php($data); 26 | //do component validation 27 | try { 28 | $this->config->validateData($data); 29 | } catch (ValidationException $e) { 30 | $result = $e->getResult(); 31 | foreach ($result->getMessages() as $message) { 32 | if (!$this->fieldHasError($message['fieldName'])) { 33 | $this->validationError($message['fieldName'], $message['message'], 'bad'); 34 | } 35 | } 36 | $valid = false; 37 | } 38 | if (!$valid) { 39 | $this->form->sessionMessage( 40 | _t( 41 | __CLASS__ . ".InvalidDataMessage", 42 | "There are problems with the data you entered. See below:" 43 | ), 44 | "bad" 45 | ); 46 | } 47 | 48 | return $valid; 49 | } 50 | 51 | public function fieldHasError($field): bool 52 | { 53 | if ($this->getErrors()) { 54 | foreach ($this->getErrors() as $error) { 55 | if ($error['fieldName'] === $field) { 56 | return true; 57 | } 58 | } 59 | } 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/OrderAttribute.php: -------------------------------------------------------------------------------- 1 | 'Currency', 25 | ]; 26 | 27 | private static array $has_one = [ 28 | 'Order' => Order::class, 29 | ]; 30 | 31 | private static array $casting = [ 32 | 'TableTitle' => 'Text', 33 | 'CartTitle' => 'Text', 34 | ]; 35 | 36 | private static string $table_name = 'SilverShop_OrderAttribute'; 37 | 38 | public function canCreate($member = null, $context = []): bool 39 | { 40 | return false; 41 | } 42 | 43 | public function canDelete($member = null): bool 44 | { 45 | return false; 46 | } 47 | 48 | public function isLive(): bool 49 | { 50 | if (!$this->isInDB()) { 51 | return true; 52 | } 53 | return $this->Order()->exists() && $this->Order()->IsCart(); 54 | } 55 | 56 | /** 57 | * Produces a title for use in templates. 58 | */ 59 | public function getTableTitle(): string 60 | { 61 | $title = $this->i18n_singular_name(); 62 | $this->extend('updateTableTitle', $title); 63 | return $title; 64 | } 65 | 66 | public function getCartTitle(): string 67 | { 68 | $title = $this->getTableTitle(); 69 | $this->extend('updateCartTitle', $title); 70 | return $title; 71 | } 72 | 73 | public function ShowInTable(): bool 74 | { 75 | $showInTable = true; 76 | $this->extend('updateShowInTable', $showInTable); 77 | return $showInTable; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/php/Fixtures/variations.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Model\Variation\AttributeType: 2 | size: 3 | Name: Ball Size 4 | Label: Size 5 | color: 6 | Name: Color 7 | Label: Color 8 | capacity: 9 | Name: Device Capacity 10 | Label: Capacity 11 | shape: 12 | Name: Shape 13 | Label: Shape 14 | 15 | SilverShop\Model\Variation\AttributeValue: 16 | size_small: 17 | Value: Small 18 | Type: =>SilverShop\Model\Variation\AttributeType.size 19 | size_medium: 20 | Value: Medium 21 | Type: =>SilverShop\Model\Variation\AttributeType.size 22 | size_large: 23 | Value: Large 24 | Type: =>SilverShop\Model\Variation\AttributeType.size 25 | color_red: 26 | Value: Red 27 | Type: =>SilverShop\Model\Variation\AttributeType.color 28 | color_blue: 29 | Value: Blue 30 | Type: =>SilverShop\Model\Variation\AttributeType.color 31 | color_yellow: 32 | Value: Yellow 33 | Type: =>SilverShop\Model\Variation\AttributeType.color 34 | capacity_60: 35 | Value: 60GB 36 | Type: =>SilverShop\Model\Variation\AttributeType.capacity 37 | capacity_120: 38 | Value: 120GB 39 | Type: =>SilverShop\Model\Variation\AttributeType.capacity 40 | 41 | SilverShop\Page\Product: 42 | ball: 43 | Title: Beach Ball 44 | BasePrice: 22.00 45 | VariationAttributeTypes: =>SilverShop\Model\Variation\AttributeType.size,=>SilverShop\Model\Variation\AttributeType.color 46 | mp3player: 47 | Title: Mp3 Player 48 | BasePrice: 200 49 | VariationAttributeTypes: =>SilverShop\Model\Variation\AttributeType.capacity,=>SilverShop\Model\Variation\AttributeType.color 50 | 51 | SilverShop\Model\Variation\Variation: 52 | redLarge: 53 | Product: =>SilverShop\Page\Product.ball 54 | Price: 22.00 55 | AttributeValues: =>SilverShop\Model\Variation\AttributeValue.size_large,=>SilverShop\Model\Variation\AttributeValue.color_red 56 | redSmall: 57 | Product: =>SilverShop\Page\Product.ball 58 | Price: 20.00 59 | AttributeValues: =>SilverShop\Model\Variation\AttributeValue.size_small,=>SilverShop\Model\Variation\AttributeValue.color_red 60 | -------------------------------------------------------------------------------- /templates/SilverShop/Page/Layout/Product.ss: -------------------------------------------------------------------------------- 1 |
2 |

$Title

3 | 4 |
5 | <% if $Image.ContentImage %> 6 | <%t SilverShop\Page\Product.ImageAltText " /> 7 | <% else %> 8 |
<%t SilverShop\Page\Product.NoImage "no image" %>
9 | <% end_if %> 10 | <% if $InternalItemID %> 11 |

12 | <%t SilverShop\Page\Product.Code "Product Code" %>: 13 | {$InternalItemID} 14 |

15 | <% end_if %> 16 | <% if $Model %> 17 |

18 | <%t SilverShop\Page\Product.Model "Model" %>: 19 | $Model.XML 20 |

21 | <% end_if %> 22 | <% if $Size %> 23 |

24 | <%t SilverShop\Page\Product.Size "Size" %>: 25 | $Size.XML 26 |

27 | <% end_if %> 28 | <% include SilverShop\Includes\Price %> 29 | <% if $IsInCart %> 30 |

31 | <% if $Item.Quantity == 1 %> 32 | <%t SilverShop\Page\Product.NumItemsInCartSingular "You have this item in your cart" %> 33 | <% else %> 34 | <%t SilverShop\Page\Product.NumItemsInCartPlural "You have {Quantity} items in your cart" Quantity=$Item.Quantity %> 35 | <% end_if %> 36 |

37 | <% end_if %> 38 | $Form 39 |
40 | <% if $Content %> 41 |
42 | $Content 43 |
44 | <% end_if %> 45 |
46 | <% include SilverShop\Includes\SideBar %> 47 | 48 | -------------------------------------------------------------------------------- /client/src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silvershop", 3 | "css_prefix_text": "icon-", 4 | "css_use_suffix": false, 5 | "hinting": true, 6 | "units_per_em": 1000, 7 | "ascent": 850, 8 | "glyphs": [ 9 | { 10 | "uid": "140ec4e8533c39ed642256d6aa879540", 11 | "css": "catalog", 12 | "code": 59393, 13 | "src": "custom_icons", 14 | "selected": true, 15 | "svg": { 16 | "path": "M0 62.5L468.8 187.5 468.8 875 0 750ZM1000 62.5L531.3 187.5 531.3 875 1000 750Z", 17 | "width": 1000 18 | }, 19 | "search": [ 20 | "catalog" 21 | ] 22 | }, 23 | { 24 | "uid": "e05b61aa959764d98094a7ab7c119656", 25 | "css": "zones", 26 | "code": 59394, 27 | "src": "custom_icons", 28 | "selected": true, 29 | "svg": { 30 | "path": "M875 687.5H739.4C725.3 708.1 709.8 729 693.5 750H832.7L907.7 937.5H92.3L167.3 750H306.5C290.3 729 274.6 708.1 260.6 687.5H125L0 1000H1000L875 687.5ZM500 875C291.7 666.7 187.5 479.2 187.5 312.5 187.5 139.9 327.4 0 500 0S812.5 139.9 812.5 312.5C812.5 479.2 708.3 666.7 500 875ZM500 437.5C569 437.5 625 381.5 625 312.5S569 187.5 500 187.5 375 243.5 375 312.5 431 437.5 500 437.5Z", 31 | "width": 1000 32 | }, 33 | "search": [ 34 | "zones" 35 | ] 36 | }, 37 | { 38 | "uid": "b6c9f366467b86e15426a7021894b551", 39 | "css": "cart", 40 | "code": 59395, 41 | "src": "custom_icons", 42 | "selected": true, 43 | "svg": { 44 | "path": "M218.1 750A31.1 31.1 0 0 1 205.9 747.3C190.1 740.1 183.1 721.6 190.3 705.9L245.4 583.8 195 184.7 148.8 93.7 31.2 93.7C13.9 93.7-0.1 79.7-0.1 62.5S13.9 31.2 31.2 31.2L187.1 31.2 234.8 125 937.4 125 874.3 524.8 312.8 586.5 267.1 687.5 875 687.5 875 750 219.4 750A33 33 0 0 1 219.4 750C270.9 750.3 312.6 792.2 312.6 843.8 312.6 895.5 270.6 937.5 218.8 937.5S125.1 895.5 125.1 843.8C125.1 792.2 166.7 750.3 218.2 750ZM843.8 937.5C792 937.5 750 895.5 750 843.8S792 750 843.8 750 937.5 792 937.5 843.8 895.5 937.5 843.8 937.5Z", 45 | "width": 1000 46 | }, 47 | "search": [ 48 | "cart" 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /src/Checkout/Component/Terms.php: -------------------------------------------------------------------------------- 1 | TermsPage(); 17 | 18 | if ($page->exists()) { 19 | $fieldList->push( 20 | CheckboxField::create( 21 | 'ReadTermsAndConditions', 22 | DBField::create_field('HTMLText', _t( 23 | 'SilverShop\Checkout\Checkout.TermsAndConditionsLink', 24 | 'I agree to the terms and conditions stated on the {TermsPageTitle} page', 25 | '', 26 | ['TermsPageLink' => $page->Link(), 'TermsPageTitle' => $page->Title] 27 | )) 28 | )->setCustomValidationMessage( 29 | _t('SilverShop\Checkout\CheckoutField.MustAgreeToTerms', 'You must agree to the terms and conditions') 30 | ) 31 | ); 32 | } 33 | 34 | return $fieldList; 35 | } 36 | 37 | public function validateData(Order $order, array $data): bool 38 | { 39 | return true; 40 | } 41 | 42 | public function getData(Order $order): array 43 | { 44 | return []; 45 | } 46 | 47 | public function setData(Order $order, array $data): Order 48 | { 49 | return $order; 50 | } 51 | 52 | public function getRequiredFields(Order $order): array 53 | { 54 | $fields = parent::getRequiredFields($order); 55 | 56 | if (SiteConfig::current_site_config()->TermsPage()->exists()) { 57 | $fields[] = 'ReadTermsAndConditions'; 58 | } 59 | 60 | return $fields; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/php/Fixtures/Addresses.yml: -------------------------------------------------------------------------------- 1 | SilverShop\Model\Address: 2 | wnz6012: 3 | Country: NZ 4 | State: Wellington 5 | PostalCode: 6012 6 | #Has an un-trimmed + uppercase state 7 | wnz6022: 8 | Country: NZ 9 | State: ' WELLINGTON ' 10 | PostalCode: 6022 11 | anz1010: 12 | Country: NZ 13 | State: Auckland 14 | PostalCode: 1010 15 | #a cross-section of international addresses 16 | #sourced from: http://www.bitboost.com/ref/international-address-formats.html 17 | bukhp193eq: 18 | Company: 'Nildram Ltd' 19 | Address: 'Ardenham Court' 20 | Address2: 'Oxford Road' 21 | City: AYLESBURY 22 | State: BUCKINGHAMSHIRE 23 | PostalCode: 'HP19 3EQ' 24 | Country: UK 25 | #'Uses 'PostCode' instead of 'PostalCode','Territory' instaed of 'State', and 'District' instead of 'City' 26 | sau5024: 27 | Name: 'MR W HEISENBERG' 28 | Address2: 'U 235' 29 | Address: '201-203 BROADWAY AVE' 30 | District: 'WEST BEACH' 31 | Territory: SA 32 | PostCode: 5024 33 | Country: AU 34 | scn266033: 35 | Name: 'MR. ZHIMIN LI' 36 | Address: '63 RENMIN LU' 37 | City: 'QINGDAO SHI' 38 | Province: SHANDONG 39 | PostCode: 266033 40 | Country: CN 41 | #Swizerland - doesn't have a state / province 42 | zch1234: 43 | Title: Herr 44 | Name: 'Hans Katze' 45 | Address: 'Tastentanzenstrasse 5/16' 46 | PostalCode: 1234 47 | City: Zuerich 48 | Country: CH 49 | aus85728: 50 | Name: NISWANDEE 51 | Company: BITBOOST 52 | Address: 'POB 65502' 53 | City: TUCSON 54 | State: ARIZONA 55 | ZipCode: 85728 56 | Country: US 57 | wus98102: 58 | Name: 'JOHN SMITH' 59 | Address: '300 BOYLSTON AVE E' 60 | City: SEATTLE 61 | State: WA 62 | ZipCode: 98102 63 | Country: US 64 | nus11230: 65 | Name: Bobbie O'Neil 66 | Address: '1000 Coney Island Ave' 67 | City: Brooklyn 68 | State: NY 69 | ZipCode: 11230 70 | Country: US 71 | -------------------------------------------------------------------------------- /tests/php/Tasks/CartCleanupTaskTest.php: -------------------------------------------------------------------------------- 1 | set(CartCleanupTask::class, 'delete_after_mins', 120); 23 | DBDatetime::set_mock_now('2014-01-31 13:00:00'); 24 | 25 | // less than two hours old 26 | $orderRunningRecent = Order::create()->update(['Status' => 'Cart']); 27 | $orderRunningRecentID = $orderRunningRecent->write(); 28 | DB::query('UPDATE "SilverShop_Order" SET "LastEdited" = \'2014-01-31 12:30:00\' WHERE "ID" = ' . $orderRunningRecentID); 29 | 30 | // three hours old 31 | $orderRunningOld = Order::create()->update(['Status' => 'Cart']); 32 | $orderRunningOldID = $orderRunningOld->write(); 33 | DB::query('UPDATE "SilverShop_Order" SET "LastEdited" = \'2014-01-31 10:00:00\' WHERE "ID" = ' . $orderRunningOldID); 34 | 35 | // three hours old 36 | $orderPaidOld = Order::create()->update(['Status' => 'Paid']); 37 | $orderPaidOldID = $orderPaidOld->write(); 38 | DB::query('UPDATE "SilverShop_Order" SET "LastEdited" = \'2014-01-31 10:00:00\' WHERE "ID" = ' . $orderPaidOldID); 39 | 40 | $fakeCartCleanupTask = FakeCartCleanupTask::create(); 41 | $fakeCartCleanupTask->run(null); 42 | 43 | $this->assertInstanceOf(Order::class, Order::get()->byID($orderRunningRecentID)); 44 | $this->assertNull(Order::get()->byID($orderRunningOldID)); 45 | $this->assertInstanceOf(Order::class, Order::get()->byID($orderPaidOldID)); 46 | 47 | $this->assertEquals('1 old carts removed.', $fakeCartCleanupTask->log[count($fakeCartCleanupTask->log) - 1]); 48 | 49 | DBDatetime::clear_mock_now(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Extension/ViewableCartExtension.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ViewableCartExtension extends Extension 25 | { 26 | /** 27 | * Get the cart, and do last minute calculation if necessary. 28 | */ 29 | public function Cart(): false|Order 30 | { 31 | $order = ShoppingCart::curr(); 32 | if (!$order instanceof Order || !$order->Items() || !$order->Items()->exists()) { 33 | return false; 34 | } 35 | 36 | return $order; 37 | } 38 | 39 | public function getContinueLink(): string 40 | { 41 | if ($cartPage = CartPage::get()->first()) { 42 | if ($cartPage->ContinuePageID && $cartPage->ContinuePage()->exists()) { 43 | return $cartPage->ContinuePage()->Link(); 44 | } 45 | } 46 | 47 | $maincategory = ProductCategory::get() 48 | ->sort( 49 | [ 50 | 'ParentID' => 'ASC', 51 | 'ID' => 'ASC', 52 | ] 53 | )->first(); 54 | if ($maincategory) { 55 | return $maincategory->Link(); 56 | } 57 | 58 | return Director::baseURL(); 59 | } 60 | 61 | public function getCartLink(): string 62 | { 63 | return CartPage::find_link(); 64 | } 65 | 66 | public function getCheckoutLink(): string 67 | { 68 | return CheckoutPage::find_link(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Page/ProductCategoryController.php: -------------------------------------------------------------------------------- 1 | ProductsShowable($recursive); 18 | 19 | //sort the products 20 | $products = $this->getSorter()->sortList($products); 21 | 22 | //paginate the products, if necessary 23 | $pagelength = ProductCategory::config()->page_length; 24 | if ($pagelength > 0) { 25 | $products = PaginatedList::create($products, $this->request); 26 | $products->setPageLength($pagelength); 27 | $products->TotalCount = $products->getTotalItems(); 28 | } 29 | 30 | return $products; 31 | } 32 | 33 | /** 34 | * Return products that are featured, that is products that have "FeaturedProduct = 1" 35 | */ 36 | public function FeaturedProducts($recursive = true): DataList 37 | { 38 | return $this->ProductsShowable($recursive) 39 | ->filter('Featured', true); 40 | } 41 | 42 | /** 43 | * Return products that are not featured, that is products that have "FeaturedProduct = 0" 44 | */ 45 | public function NonFeaturedProducts($recursive = true): DataList 46 | { 47 | return $this->ProductsShowable($recursive) 48 | ->filter('Featured', false); 49 | } 50 | 51 | /** 52 | * Sorting controls 53 | */ 54 | public function getSorter(): ListSorter 55 | { 56 | $options = []; 57 | foreach (ProductCategory::config()->sort_options as $k => $v) { 58 | // make the label translatable 59 | $k = _t(ProductCategory::class . '.' . $k, $k); 60 | $options[$k] = $v; 61 | } 62 | 63 | $listSorter = ListSorter::create($this->request, $options); 64 | $this->extend('updateSorter', $listSorter); 65 | 66 | return $listSorter; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/dist/css/account.css: -------------------------------------------------------------------------------- 1 | /* create a box floated to the right */ 2 | .accountcontent { 3 | float: left; 4 | width: 70%; 5 | } 6 | 7 | .accountnav { 8 | float: right; 9 | width: 25%; 10 | } 11 | 12 | table.orderhistory { 13 | width: 100%; 14 | } 15 | 16 | /* 17 | * Member form styling, and tweaks to the login form as well 18 | */ 19 | #ShopAccountForm_EditAccountForm, 20 | #ChangePasswordForm_ChangePasswordForm { 21 | width: 45%; 22 | float: left; 23 | } 24 | 25 | #ChangePasswordForm_ChangePasswordForm { 26 | margin-left: 5%; 27 | } 28 | 29 | 30 | /* address book panels ************************************************************/ 31 | .AccountPage h2 { 32 | clear: left; 33 | } 34 | 35 | .address-panel { 36 | position: relative; 37 | float: left; 38 | width: 48%; 39 | background: #eee; 40 | border: 1px solid #ccc; 41 | border-radius:2px; 42 | padding: 20px; 43 | margin-bottom: 5px; 44 | } 45 | .address-panel.odd { 46 | margin-right:2%; 47 | } 48 | 49 | .address-panel .panel-body { 50 | font-size:14px; 51 | line-height:17px; 52 | } 53 | .address-panel .panel-footer { 54 | border-top: 1px solid #d9d9d9; 55 | padding: 8px 20px; 56 | bottom: 0px; 57 | margin: 15px -20px -20px; 58 | } 59 | 60 | /*@include clearfix;*/ 61 | .cf:before, 62 | .cf:after { 63 | content: " "; /* 1 */ 64 | display: table; /* 2 */ 65 | } 66 | .cf:after { 67 | clear: both; 68 | } 69 | 70 | .address-panel .panel-footer .btn { 71 | line-height: 32px; 72 | padding: 0 9px; 73 | font-size: 10px; 74 | margin: 0 9px 0 0; 75 | } 76 | 77 | .address-panel .remove-address { 78 | float: right; 79 | line-height: 32px; 80 | border:none; 81 | } 82 | 83 | .address-panel .remove-address img { 84 | background: none; 85 | border: none; 86 | } 87 | 88 | .address-panel .tag { 89 | position: relative; 90 | top: -21px; 91 | background: #f7931e; 92 | color: white; 93 | font-size: 10px; 94 | padding: 4px 10px; 95 | border-radius: 0 0 2px 2px; 96 | float: right; 97 | right: -10px; 98 | margin-left: 10px; 99 | } 100 | -------------------------------------------------------------------------------- /src/Checkout/Step/PaymentMethod.php: -------------------------------------------------------------------------------- 1 | addComponent(Payment::create()); 26 | 27 | return $checkoutComponentConfig; 28 | } 29 | 30 | public function paymentmethod(): HTTPResponse|array 31 | { 32 | $gateways = GatewayInfo::getSupportedGateways(); 33 | if (count($gateways) == 1) { 34 | return $this->owner->redirect($this->NextStepLink()); 35 | } 36 | return [ 37 | 'OrderForm' => $this->PaymentMethodForm(), 38 | ]; 39 | } 40 | 41 | public function PaymentMethodForm(): CheckoutForm 42 | { 43 | $checkoutForm = CheckoutForm::create($this->owner, 'PaymentMethodForm', $this->checkoutconfig()); 44 | $checkoutForm->setActions( 45 | FieldList::create( 46 | FormAction::create('setpaymentmethod', _t('SilverShop\Checkout\Step\CheckoutStep.Continue', 'Continue')) 47 | ) 48 | ); 49 | $this->owner->extend('updatePaymentMethodForm', $checkoutForm); 50 | 51 | return $checkoutForm; 52 | } 53 | 54 | public function setpaymentmethod($data, $form): HTTPResponse 55 | { 56 | $this->checkoutconfig()->setData($form->getData()); 57 | return $this->owner->redirect($this->NextStepLink()); 58 | } 59 | 60 | public function SelectedPaymentMethod(): string|array 61 | { 62 | return Checkout::get($this->owner->Cart())->getSelectedPaymentMethod(true); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Tasks/CustomersToGroupTask.php: -------------------------------------------------------------------------------- 1 | CustomerGroup(); 26 | if (!$gp->exists()) { 27 | die( 28 | _t( 29 | 'SilverShop\Task\CustomersToGroupTask.DefaultCustomerGroupRequired', 30 | 'Default Customer Group required' 31 | ) 32 | ); 33 | } 34 | 35 | $query = DB::query( 36 | 'SELECT "ID", "MemberID", "GroupID" FROM "Group_Members" WHERE "Group_Members"."GroupID" = ' 37 | . $gp->ID . ';' 38 | ); 39 | //make an array of all combos 40 | $alreadyAdded = []; 41 | $alreadyAdded[-1] = -1; 42 | if ($query) { 43 | foreach ($query as $combo) { 44 | $alreadyAdded[$combo['MemberID']] = $combo['MemberID']; 45 | } 46 | } 47 | $dataList = DataObject::get( 48 | Member::class, 49 | $where = '"Member"."ID" NOT IN (' . implode(',', $alreadyAdded) . ')', 50 | $sort = null, 51 | )->leftJoin( 52 | 'SilverShop_Order', 53 | $join = '"SilverShop_Order"."MemberID" = "Member"."ID"' 54 | ); 55 | //add combos 56 | if ($dataList) { 57 | $existingMembers = $gp->Members(); 58 | foreach ($dataList as $member) { 59 | $existingMembers->add($member); 60 | echo '.'; 61 | } 62 | } else { 63 | echo _t( 64 | 'SilverShop\Task\CustomersToGroupTask.NoNewMembersAdded', 65 | 'No new members added' 66 | ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Tasks/ShopEmailPreviewTask.php: -------------------------------------------------------------------------------- 1 | 15 | * @date September 2016 16 | * @package shop 17 | * @subpackage tasks 18 | */ 19 | 20 | /** 21 | * ShopEmailPreviewTask 22 | */ 23 | class ShopEmailPreviewTask extends BuildTask 24 | { 25 | protected $title = 'Preview Shop Emails'; 26 | 27 | protected $description = 'Previews shop emails'; 28 | 29 | protected $previewableEmails = [ 30 | 'Confirmation', 31 | 'Receipt', 32 | 'AdminNotification', 33 | 'CancelNotification', 34 | 'StatusChange' 35 | ]; 36 | 37 | /** 38 | * @param HTTPRequest $request 39 | */ 40 | public function run($request): void 41 | { 42 | $email = $request->remaining(); 43 | $params = $request->allParams(); 44 | $url = Director::absoluteURL("dev/{$params['Action']}/{$params['TaskName']}"); 45 | $debug = true; 46 | 47 | if ($request->getVar('debug')) { 48 | $debug = $request->getVar('debug'); 49 | } 50 | 51 | echo '

Choose Email

'; 52 | echo '
'; 57 | 58 | if ($email && in_array($email, $this->previewableEmails)) { 59 | $order = Order::get()->first(); 60 | $notifier = OrderEmailNotifier::create($order); 61 | 62 | if ($debug) { 63 | $notifier->setDebugMode(true); 64 | } 65 | 66 | $method = "send$email"; 67 | 68 | if ($email == 'StatusChange') { 69 | echo $notifier->$method('This is a test title', 'This is a test note'); 70 | } else { 71 | echo $notifier->$method(); 72 | } 73 | } 74 | //this is a little hardcore way of ending the party, 75 | //but as it's only used for styling, it works for now 76 | die; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Forms/GridField/OrderGridFieldDetailForm_ItemRequest.php: -------------------------------------------------------------------------------- 1 | Link('printorder') . '?print=1'; 27 | $printwindowjs = <<Actions()->push( 31 | LiteralField::create( 32 | 'PrintOrder', 33 | "' 35 | ) 36 | ); 37 | 38 | return $form; 39 | } 40 | 41 | /** 42 | * Render order for printing 43 | */ 44 | public function printorder(): DBHTMLText 45 | { 46 | Requirements::clear(); 47 | //include print javascript, if print argument is provided 48 | if (isset($_REQUEST['print']) && $_REQUEST['print']) { 49 | Requirements::customScript('if(document.location.href.indexOf(\'print=1\') > 0) {window.print();}'); 50 | } 51 | $title = _t('SilverShop\Model\Order.Invoice', 'Invoice'); 52 | if ($id = $this->popupController->getRequest()->param('ID')) { 53 | $title .= " #$id"; 54 | } 55 | 56 | return $this->record->customise( 57 | [ 58 | 'SiteConfig' => SiteConfig::current_site_config(), 59 | 'Title' => $title, 60 | ] 61 | )->renderWith('SilverShop\Admin\OrderAdmin_Printable'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Reports/CustomerReport.php: -------------------------------------------------------------------------------- 1 | 'First Name', 30 | 'Surname' => 'Surname', 31 | 'Email' => 'Email', 32 | 'Created' => 'Joined', 33 | 'Spent' => 'Spent', 34 | 'Orders' => 'Orders', 35 | 'edit' => [ 36 | 'title' => 'Edit', 37 | 'formatting' => 'edit', 38 | ], 39 | ]; 40 | } 41 | 42 | public function getReportField(): FormField 43 | { 44 | return parent::getReportField(); 45 | } 46 | 47 | public function query($params): ShopReportQuery|SQLSelect 48 | { 49 | $query = parent::query($params); 50 | $query->selectField($this->periodfield, 'FilterPeriod') 51 | ->addSelect( 52 | ['"Member"."ID"', '"Member"."FirstName"', '"Member"."Surname"', '"Member"."Email"', '"Member"."Created"'] 53 | ) 54 | ->selectField('COUNT("SilverShop_Order"."ID")', 'Orders') 55 | ->selectField('SUM("SilverShop_Order"."Total")', 'Spent'); 56 | 57 | $query->addInnerJoin('SilverShop_Order', '"Member"."ID" = "SilverShop_Order"."MemberID"'); 58 | 59 | $query->addGroupBy('"Member"."ID"'); 60 | 61 | if (!$query->getOrderBy()) { 62 | $query->setOrderBy( 63 | [ 64 | 'Spent' => 'DESC', 65 | 'Orders' => 'DESC' 66 | ] 67 | ); 68 | } 69 | $query->setLimit(50); 70 | return $query; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Checkout/Component/CustomerDetails.php: -------------------------------------------------------------------------------- 1 | getRequiredFields($order) as $field_name) { 34 | if (!isset($field_name)) { 35 | $validationResult->addError( 36 | _t(__CLASS__ . '.No' . $field_name, "{$field_name} is required"), 37 | "CustomerDetails" 38 | ); 39 | throw ValidationException::create($validationResult); 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | public function getData(Order $order): array 46 | { 47 | if ($order->FirstName || $order->Surname || $order->Email) { 48 | return [ 49 | 'FirstName' => $order->FirstName, 50 | 'Surname' => $order->Surname, 51 | 'Email' => $order->Email, 52 | ]; 53 | } 54 | if ($member = Security::getCurrentUser()) { 55 | return [ 56 | 'FirstName' => $member->FirstName, 57 | 'Surname' => $member->Surname, 58 | 'Email' => $member->Email, 59 | ]; 60 | } 61 | return []; 62 | } 63 | 64 | public function setData(Order $order, array $data): Order 65 | { 66 | $order->update($data); 67 | $order->write(); 68 | return $order; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /templates/SilverShop/Cart/SideCart.ss: -------------------------------------------------------------------------------- 1 | <% require css("silvershop/core: client/dist/css/sidecart.css") %> 2 | 3 |
4 |

<%t SilverShop\Cart\ShoppingCart.Headline "Shopping cart" %>

5 | <% if $Cart %> 6 | <% with $Cart %> 7 |

8 | <% if $Items.Plural %> 9 | <%t SilverShop\Cart\ShoppingCart.ItemsInCartPlural 'There are {quantity} items in your cart.' link=$Top.CartLink quantity=$Items.Quantity %> 10 | <% else %> 11 | <%t SilverShop\Cart\ShoppingCart.ItemsInCartSingular 'There is 1 item in your cart.' link=$Top.CartLink %> 12 | <% end_if %> 13 |

14 | 17 | <% loop $Items %> 18 |
19 | <% if $Product.Image %> 20 | 25 | <% end_if %> 26 |

27 | "> 28 | $TableTitle 29 | 30 |

31 |

$Quantity x $UnitPrice.Nice

32 | <% if $SubTitle %>

$SubTitle

<% end_if %> 33 | ">x 34 |
35 | <% end_loop %> 36 | <% end_with %> 37 | <% else %> 38 |

<%t SilverShop\Cart\ShoppingCart.NoItems "There are no items in your cart." %>

39 | <% end_if %> 40 |
41 | -------------------------------------------------------------------------------- /src/Model/Modifiers/Tax/GlobalTax.php: -------------------------------------------------------------------------------- 1 | 'Varchar', 17 | ]; 18 | 19 | /** 20 | * Tax rates per country 21 | */ 22 | private static array $country_rates = []; 23 | 24 | private static string $table_name = 'SilverShop_GlobalTaxModifier'; 25 | 26 | public function value($incoming): int|float 27 | { 28 | $rate = $this->Type == 'Chargable' 29 | ? 30 | $this->Rate() 31 | : 32 | round(1 - (1 / (1 + $this->Rate())), Order::config()->rounding_precision); 33 | return $incoming * $rate; 34 | } 35 | 36 | public function Rate() 37 | { 38 | // If the order is no longer in cart, rely on the saved data 39 | if ($this->OrderID && $this->Order()->exists() && !$this->Order()->IsCart()) { 40 | return $this->getField('Rate'); 41 | } 42 | 43 | $rates = self::config()->country_rates; 44 | $country = $this->Country(); 45 | if ($country && isset($rates[$country])) { 46 | return $this->Rate = $rates[$country]['rate']; 47 | } 48 | $defaults = self::config()->defaults; 49 | return $this->Rate = $defaults['Rate']; 50 | } 51 | 52 | public function getTableTitle(): string 53 | { 54 | $country = $this->Country() ? ' (' . $this->Country() . ') ' : ''; 55 | 56 | return parent::getTableTitle() . $country . 57 | ($this->Type == 'Chargable' ? '' : _t(__CLASS__ . '.Included', ' (included in the above price)')); 58 | } 59 | 60 | public function Country() 61 | { 62 | if ($this->OrderID && $this->Order()->exists() && $address = $this->Order()->getBillingAddress()) { 63 | return $address->Country; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | public function onBeforeWrite(): void 70 | { 71 | parent::onBeforeWrite(); 72 | // While the order is still in "Cart" status, persist country code to DB 73 | if ($this->OrderID && $this->Order()->exists() && $this->Order()->IsCart()) { 74 | $this->setField('Country', $this->Country()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/php/Model/AddressTest.php: -------------------------------------------------------------------------------- 1 | update( 14 | [ 15 | 'Country' => 'NZ', 16 | 'State' => 'Wellington', 17 | 'City' => 'TeAro', 18 | 'PostalCode' => '1333', 19 | 'Address' => '23 Blah Street', 20 | 'AddressLine2' => 'Fitzgerald Building, Foor 3', 21 | 'Company' => 'Ink inc', 22 | 'FirstName' => 'Jerald', 23 | 'Surname' => 'Smith', 24 | 'Phone' => '12346678', 25 | ] 26 | ); 27 | $this->assertEquals( 28 | "Ink inc|Jerald Smith|23 Blah Street|Fitzgerald Building, Foor 3|TeAro|Wellington|1333|NZ", 29 | $address->toString("|") 30 | ); 31 | } 32 | 33 | public function testRequiredFields(): void 34 | { 35 | // create address instance that lacks some required fields (Address) 36 | $address = Address::create()->update( 37 | [ 38 | 'Country' => 'NZ', 39 | 'State' => 'Wellington', 40 | 'City' => 'TeAro', 41 | ] 42 | ); 43 | 44 | $writeFailed = false; 45 | try { 46 | $address->write(); 47 | } catch (Exception $ex) { 48 | $writeFailed = true; 49 | } 50 | 51 | $this->assertTrue($writeFailed, "Address should not be writable, since it doesn't contain all required fields"); 52 | 53 | // Create an Address that satisfies the baseline required fields, but not the ones that were added via subclass. 54 | $address = ExtendedTestAddress::create()->update( 55 | [ 56 | 'Country' => 'NZ', 57 | 'State' => 'Wellington', 58 | 'City' => 'TeAro', 59 | 'Address' => '23 Blah Street', 60 | ] 61 | ); 62 | 63 | $writeFailed = false; 64 | try { 65 | $address->write(); 66 | } catch (Exception $ex) { 67 | $writeFailed = true; 68 | } 69 | 70 | $this->assertTrue( 71 | $writeFailed, 72 | "Address should not be writable, since it doesn't contain required fields added via subclass" 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/php/Page/CheckoutPageTest.php: -------------------------------------------------------------------------------- 1 | objFromFixture(Order::class, "unpaid"); 30 | OrderManipulationExtension::add_session_order($order); 31 | $this->get("/checkout/order/" . $order->ID); 32 | 33 | //make payment action 34 | $this->post( 35 | "/checkout/order/ActionsForm", 36 | [ 37 | 'OrderID' => $order->ID, 38 | 'PaymentMethod' => 'Dummy', 39 | 'action_dopayment' => 'submit', 40 | ] 41 | ); 42 | 43 | //cancel action 44 | $this->post( 45 | "/checkout/order/ActionsForm", 46 | [ 47 | 'OrderID' => $order->ID, 48 | 'action_docancel' => 'submit', 49 | ] 50 | ); 51 | 52 | $order = Order::get()->byID($order->ID); 53 | $this->assertNull($order->PaymentStatus, 'Payment status should be null after cancellation'); 54 | $this->assertEquals('Unpaid', $order->Status, 'Order status should be Unpaid'); 55 | } 56 | 57 | public function testCanViewCheckoutPage(): void 58 | { 59 | $httpResponse = $this->get('checkout'); 60 | $this->assertEquals(404, $httpResponse->getStatusCode(), 'Cannot access the Checkout Page without a current order'); 61 | } 62 | 63 | public function testFindLink(): void 64 | { 65 | $dataObject = $this->objFromFixture(CheckoutPage::class, 'checkout'); 66 | $dataObject->publishSingle(); 67 | $link = CheckoutPage::find_link(); 68 | $this->assertEquals( 69 | Director::baseURL() . 'checkout', 70 | $link, 71 | 'find_link() returns the correct link to checkout.' 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/php/Checkout/OrderEmailNotifierTest.php: -------------------------------------------------------------------------------- 1 | set(Email::class, 'admin_email', 'shop-admin@example.com'); 33 | // clear any setting that might have been made via shop-config 34 | Config::modify()->remove(ShopConfigExtension::class, 'email_from'); 35 | $this->order = $this->objFromFixture(Order::class, 'paid'); 36 | $this->notifier = OrderEmailNotifier::create($this->order); 37 | } 38 | 39 | public function testAdminNotification(): void 40 | { 41 | $this->notifier->sendAdminNotification(); 42 | $this->assertEmailSent('shop-admin@example.com', 'shop-admin@example.com'); 43 | } 44 | 45 | public function testConfirmation(): void 46 | { 47 | $this->notifier->sendConfirmation(); 48 | $this->assertEmailSent('test@example.com', 'shop-admin@example.com'); 49 | } 50 | 51 | public function testReceipt(): void 52 | { 53 | $this->notifier->sendReceipt(); 54 | $this->assertEmailSent('test@example.com', 'shop-admin@example.com'); 55 | } 56 | 57 | public function testReceiptNoEmailSent(): void 58 | { 59 | $this->clearEmails(); 60 | Config::modify()->set(Order::class, 'send_receipt', false); 61 | $order = $this->objFromFixture(Order::class, 'unpaid'); 62 | $order->setField('Status', 'Paid'); 63 | $order->write(); 64 | $this->assertNull( 65 | $this->findEmail('hi@there.net', 'shop-admin@example.com'), 66 | 'An email is not sent when the Order class send_receipt is set to false' 67 | ); 68 | } 69 | 70 | public function testStatusUpdate(): void 71 | { 72 | $this->notifier->sendStatusChange('test subject'); 73 | $this->assertEmailSent('test@example.com', 'shop-admin@example.com', 'Silvershop - test subject'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Checkout/Step/ContactDetails.php: -------------------------------------------------------------------------------- 1 | ContactDetailsForm(); 31 | if (!ShoppingCart::curr() instanceof Order) { 32 | return [ 33 | 'OrderForm' => $form, 34 | ]; 35 | } 36 | if (!self::config()->skip_if_logged_in) { 37 | return [ 38 | 'OrderForm' => $form, 39 | ]; 40 | } 41 | if (!Security::getCurrentUser()) { 42 | return [ 43 | 'OrderForm' => $form, 44 | ]; 45 | } 46 | if ($form->getValidator()->validate()->isValid()) { 47 | return Controller::curr()->redirect($this->NextStepLink()); 48 | } 49 | $form->clearMessage(); 50 | 51 | return [ 52 | 'OrderForm' => $form, 53 | ]; 54 | } 55 | 56 | public function ContactDetailsForm(): false|CheckoutForm 57 | { 58 | $cart = ShoppingCart::curr(); 59 | if (!$cart instanceof Order) { 60 | return false; 61 | } 62 | $checkoutComponentConfig = CheckoutComponentConfig::create(ShoppingCart::curr()); 63 | $checkoutComponentConfig->addComponent(CustomerDetails::create()); 64 | $checkoutForm = CheckoutForm::create($this->owner, 'ContactDetailsForm', $checkoutComponentConfig); 65 | $checkoutForm->setRedirectLink($this->NextStepLink()); 66 | $checkoutForm->setActions( 67 | FieldList::create( 68 | FormAction::create('checkoutSubmit', _t('SilverShop\Checkout\Step\CheckoutStep.Continue', 'Continue')) 69 | ) 70 | ); 71 | $this->owner->extend('updateContactDetailsForm', $checkoutForm); 72 | 73 | return $checkoutForm; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/php/Extension/MemberExtensionTest.php: -------------------------------------------------------------------------------- 1 | set(Member::class, 'unique_identifier_field', 'Email'); 25 | $member = MemberExtension::get_by_identifier('jeremy@example.com'); 26 | $this->assertNotNull($member); 27 | $this->assertEquals('jeremy@example.com', $member->Email); 28 | $this->assertEquals('Jeremy', $member->FirstName); 29 | } 30 | 31 | /** 32 | * @doesNotPerformAssertions 33 | */ 34 | public function testCMSFields(): void 35 | { 36 | singleton(Member::class)->getCMSFields(); 37 | singleton(Member::class)->getMemberFormFields(); 38 | } 39 | 40 | public function testPastOrders(): void 41 | { 42 | $member = $this->objFromFixture(Member::class, "joebloggs"); 43 | $pastorders = $member->getPastOrders(); 44 | $this->assertEquals(1, $pastorders->count()); 45 | } 46 | 47 | public function testLoginJoinsCart(): void 48 | { 49 | Config::modify()->set(Member::class, 'login_joins_cart', true); 50 | $order = $this->objFromFixture(Order::class, "cart"); 51 | ShoppingCart::singleton()->setCurrent($order); 52 | $member = $this->objFromFixture(Member::class, "jeremyperemy"); 53 | $this->logInAs($member); 54 | $this->assertEquals($member->ID, $order->MemberID); 55 | $this->logOut(); 56 | 57 | $this->assertNull(ShoppingCart::curr()); 58 | } 59 | 60 | public function testLoginDoesntJoinCart(): void 61 | { 62 | Config::modify()->set(Member::class, 'login_joins_cart', false); 63 | $order = $this->objFromFixture(Order::class, "cart"); 64 | ShoppingCart::singleton()->setCurrent($order); 65 | $member = $this->objFromFixture(Member::class, "jeremyperemy"); 66 | $this->logInAs($member); 67 | $this->assertEquals(0, $order->MemberID); 68 | $this->logOut(); 69 | 70 | $this->assertTrue((bool)ShoppingCart::curr()); 71 | } 72 | } 73 | --------------------------------------------------------------------------------