├── .gitignore ├── .dockerignore ├── .ruby-version ├── Procfile ├── .DS_Store ├── Gemfile ├── .github └── ISSUE_TEMPLATE.md ├── Dockerfile ├── render.yaml ├── docker_push ├── index.html ├── app.json ├── .circleci └── config.yml ├── Gemfile.lock ├── LICENSE ├── README.md └── web.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec ruby web.rb -p $PORT -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe/example-terminal-backend/HEAD/.DS_Store -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "3.1.2" 4 | 5 | gem 'dotenv' 6 | gem 'encrypted_cookie' 7 | gem 'json' 8 | gem 'sinatra' 9 | gem 'stripe', '~> 7.1.0' 10 | gem 'sinatra-cross_origin' 11 | gem 'puma' -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please only file issues here that you believe represent actual bugs in this demo code. 2 | If you're having general trouble with your Stripe integration, please email support@stripe.com for a faster response. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.1.2-alpine 2 | 3 | RUN apk add build-base 4 | RUN gem install bundler:2.3.24 5 | RUN mkdir -p /www/example-terminal-backend 6 | WORKDIR /www/example-terminal-backend 7 | COPY . . 8 | RUN bundle install 9 | EXPOSE 4567 10 | 11 | ENTRYPOINT ["ruby", "web.rb", "-o", "0.0.0.0"] 12 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: example-terminal-backend 4 | env: ruby 5 | region: oregon 6 | plan: free 7 | buildCommand: bundle install 8 | startCommand: bundle exec ruby web.rb 9 | envVars: 10 | - key: STRIPE_TEST_SECRET_KEY 11 | sync: false # placeholder for a value to be added in the dashboard 12 | -------------------------------------------------------------------------------- /docker_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TAG=$(date +"%d-%m-%Y") 4 | docker build -t stripe/example-terminal-backend:$TAG . 5 | docker build -t stripe/example-terminal-backend:latest . 6 | 7 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 8 | docker push stripe/example-terminal-backend:$TAG 9 | docker push stripe/example-terminal-backend:latest 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |9 | Great! You've successfully deployed the Stripe Terminal example backend. 10 |
11 | 12 |13 | Next, copy the URL of this page (this is your "backend URL"), and use it to run the example application. 14 |
15 | 16 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example Terminal Backend", 3 | "description": "For testing the Stripe Terminal SDK example apps. https://stripe.com/docs/terminal", 4 | "repository": "https://github.com/stripe/example-terminal-backend", 5 | "keywords": ["ios", "android", "javascript", "stripe", "terminal", "stripe terminal"], 6 | "env": { 7 | "STRIPE_TEST_SECRET_KEY": { 8 | "description": "Find this at https://dashboard.stripe.com/account/apikeys (it'll look like sk_test_****)", 9 | "required": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # Define a job to be invoked later in a workflow. 4 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 5 | jobs: 6 | push: 7 | docker: 8 | - image: circleci/buildpack-deps:stretch 9 | steps: 10 | - checkout 11 | - setup_remote_docker 12 | - run: bash docker_push 13 | 14 | # Invoke jobs via workflows 15 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 16 | workflows: 17 | test_my_app: 18 | jobs: 19 | - push: 20 | filters: 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | dotenv (2.8.1) 5 | encrypted_cookie (0.1.0) 6 | rack (>= 1.1, < 3) 7 | json (2.6.2) 8 | mustermann (3.0.0) 9 | ruby2_keywords (~> 0.0.1) 10 | nio4r (2.5.9) 11 | puma (6.3.1) 12 | nio4r (~> 2.0) 13 | rack (2.2.6.4) 14 | rack-protection (3.0.4) 15 | rack 16 | ruby2_keywords (0.0.5) 17 | sinatra (3.0.4) 18 | mustermann (~> 3.0) 19 | rack (~> 2.2, >= 2.2.4) 20 | rack-protection (= 3.0.4) 21 | tilt (~> 2.0) 22 | sinatra-cross_origin (0.4.0) 23 | stripe (7.1.0) 24 | tilt (2.0.11) 25 | 26 | PLATFORMS 27 | arm64-darwin-21 28 | x86_64-linux 29 | 30 | DEPENDENCIES 31 | dotenv 32 | encrypted_cookie 33 | json 34 | puma 35 | sinatra 36 | sinatra-cross_origin 37 | stripe (~> 7.1.0) 38 | 39 | RUBY VERSION 40 | ruby 3.1.2p20 41 | 42 | BUNDLED WITH 43 | 2.3.24 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stripe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Terminal Backend 2 | ⚠️ **Note that this backend is intended for example purposes only**. Because endpoints are not authenticated, you should not use this backend in production. 3 | 4 | This is a simple [Sinatra](http://www.sinatrarb.com/) webapp that you can use to run the [Stripe Terminal](https://stripe.com/docs/terminal) example apps. To get started, you can choose from the following options: 5 | 6 | 1. [Run it on a free Render account](#running-on-render) 7 | 2. [Run it on Heroku](#running-on-heroku) 8 | 3. [Run it locally on your machine](#running-locally-on-your-machine) 9 | 4. [Run it locally via Docker CLI](#running-locally-with-docker) 10 | 11 | ℹ️ You also need to obtain your Stripe **secret, test mode** API Key, available in the [Dashboard](https://dashboard.stripe.com/account/apikeys). Note that you must use your secret key, not your publishable key, to set up the backend. For more information on the differences between **secret** and publishable keys, see [API Keys](https://stripe.com/docs/keys). For more information on **test mode**, see [Test and live modes](https://stripe.com/docs/keys#test-live-modes). 12 | 13 | ## Running the app 14 | 15 | ### Running locally on your machine 16 | 17 | If you prefer running the backend locally, ensure you have the required [Ruby runtime](https://www.ruby-lang.org/en/documentation/installation/) version installed as per the [latest Gemfile in this repo](Gemfile). 18 | 19 | You'll also need the correct [Bundler](https://bundler.io/) version, outlined in the [Gemfile.lock](Gemfile.lock) under the `BUNDLED_WITH` directive. 20 | 21 | Clone down this repo to your computer, and then follow the steps below: 22 | 23 | 1. Create a file named `.env` within the newly cloned repo directory and add the following line: 24 | ``` 25 | STRIPE_TEST_SECRET_KEY={YOUR_API_KEY} 26 | ``` 27 | 2. In your terminal, run `bundle install` 28 | 3. Run `ruby web.rb` 29 | 4. The example backend should now be running at `http://localhost:4567` 30 | 5. Go to the [next steps](#next-steps) in this README for how to use this app 31 | 32 | ### Running locally with Docker 33 | 34 | We have a pre-built Docker image you can run locally if you're into the convenience of containers. 35 | 36 | Install [Docker Desktop](https://www.docker.com/products/docker-desktop) if you don't already have it. Then follow the steps below: 37 | 38 | 1. In your terminal, run `docker run -e STRIPE_TEST_SECRET_KEY={YOUR_API_KEY} -p 4567:4567 stripe/example-terminal-backend` (replace `{YOUR_API_KEY}` with your own test key) 39 | 2. The example backend should now be running at `http://localhost:4567` 40 | 3. Go to the [next steps](#next-steps) in this README for how to use this app 41 | 42 | ### Running on Render 43 | 44 | 1. Set up a free [render account](https://dashboard.render.com/register). 45 | 2. Click the button below to deploy the example backend. You'll be prompted to enter a name for the Render service group as well as your Stripe API key. 46 | 3. Go to the [next steps](#next-steps) in this README for how to use this app 47 | 48 | [](https://render.com/deploy?repo=https://github.com/stripe/example-terminal-backend/) 49 | 50 | ### Running on Heroku 51 | 52 | 1. Set up a [Heroku account](https://signup.heroku.com). 53 | 2. Click the button below to deploy the example backend. You'll be prompted to enter a name for the Heroku application as well as your Stripe API key. 54 | 3. Go to the [next steps](#next-steps) in this README for how to use this app 55 | 56 | [](https://heroku.com/deploy?template=https://github.com/stripe/example-terminal-backend) 57 | 58 | --- 59 | 60 | ## Next steps 61 | 62 | Next, navigate to one of our example apps. Follow the instructions in the README to set up and run the app. You'll provide the URL of the example backend you just deployed. 63 | 64 | | SDK | Example App | 65 | | :--- | :--- | 66 | | iOS | https://github.com/stripe/stripe-terminal-ios | 67 | | JavaScript | https://github.com/stripe/stripe-terminal-js-demo | 68 | | Android | https://github.com/stripe/stripe-terminal-android | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /web.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'stripe' 3 | require 'dotenv' 4 | require 'json' 5 | require 'sinatra/cross_origin' 6 | 7 | # Browsers require that external servers enable CORS when the server is at a different origin than the website. 8 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 9 | # This enables the requires CORS headers to allow the browser to make the requests from the JS Example App. 10 | configure do 11 | enable :cross_origin 12 | end 13 | 14 | before do 15 | response.headers['Access-Control-Allow-Origin'] = '*' 16 | end 17 | 18 | options "*" do 19 | response.headers["Allow"] = "GET, POST, OPTIONS" 20 | response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type, Accept, X-User-Email, X-Auth-Token" 21 | response.headers["Access-Control-Allow-Origin"] = "*" 22 | 200 23 | end 24 | 25 | Dotenv.load 26 | Stripe.api_key = ENV['STRIPE_ENV'] == 'production' ? ENV['STRIPE_SECRET_KEY'] : ENV['STRIPE_TEST_SECRET_KEY'] 27 | Stripe.api_version = '2020-03-02' 28 | 29 | def log_info(message) 30 | puts "\n" + message + "\n\n" 31 | return message 32 | end 33 | 34 | get '/' do 35 | status 200 36 | send_file 'index.html' 37 | end 38 | 39 | def validateApiKey 40 | if Stripe.api_key.nil? || Stripe.api_key.empty? 41 | return "Error: you provided an empty secret key. Please provide your test mode secret key. For more information, see https://stripe.com/docs/keys" 42 | end 43 | if Stripe.api_key.start_with?('pk') 44 | return "Error: you used a publishable key to set up the example backend. Please use your test mode secret key. For more information, see https://stripe.com/docs/keys" 45 | end 46 | if Stripe.api_key.start_with?('sk_live') 47 | return "Error: you used a live mode secret key to set up the example backend. Please use your test mode secret key. For more information, see https://stripe.com/docs/keys#test-live-modes" 48 | end 49 | return nil 50 | end 51 | 52 | # This endpoint registers a Verifone P400 reader to your Stripe account. 53 | # https://stripe.com/docs/terminal/readers/connecting/verifone-p400#register-reader 54 | post '/register_reader' do 55 | validationError = validateApiKey 56 | if !validationError.nil? 57 | status 400 58 | return log_info(validationError) 59 | end 60 | 61 | begin 62 | reader = Stripe::Terminal::Reader.create( 63 | :registration_code => params[:registration_code], 64 | :label => params[:label], 65 | :location => params[:location] 66 | ) 67 | rescue Stripe::StripeError => e 68 | status 402 69 | return log_info("Error registering reader! #{e.message}") 70 | end 71 | 72 | log_info("Reader registered: #{reader.id}") 73 | 74 | status 200 75 | # Note that returning the Stripe reader object directly creates a dependency between your 76 | # backend's Stripe.api_version and your clients, making future upgrades more complicated. 77 | # All clients must also be ready for backwards-compatible changes at any time: 78 | # https://stripe.com/docs/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible 79 | return reader.to_json 80 | end 81 | 82 | # This endpoint creates a ConnectionToken, which gives the SDK permission 83 | # to use a reader with your Stripe account. 84 | # https://stripe.com/docs/terminal/sdk/js#connection-token 85 | # https://stripe.com/docs/terminal/sdk/ios#connection-token 86 | # https://stripe.com/docs/terminal/sdk/android#connection-token 87 | # 88 | # The example backend does not currently support connected accounts. 89 | # To create a ConnectionToken for a connected account, see 90 | # https://stripe.com/docs/terminal/features/connect#direct-connection-tokens 91 | post '/connection_token' do 92 | validationError = validateApiKey 93 | if !validationError.nil? 94 | status 400 95 | return log_info(validationError) 96 | end 97 | 98 | begin 99 | token = Stripe::Terminal::ConnectionToken.create 100 | rescue Stripe::StripeError => e 101 | status 402 102 | return log_info("Error creating ConnectionToken! #{e.message}") 103 | end 104 | 105 | content_type :json 106 | status 200 107 | return {:secret => token.secret}.to_json 108 | end 109 | 110 | # This endpoint creates a PaymentIntent. 111 | # https://stripe.com/docs/terminal/payments#create 112 | # 113 | # The example backend does not currently support connected accounts. 114 | # To create a PaymentIntent for a connected account, see 115 | # https://stripe.com/docs/terminal/features/connect#direct-payment-intents-server-side 116 | post '/create_payment_intent' do 117 | validationError = validateApiKey 118 | if !validationError.nil? 119 | status 400 120 | return log_info(validationError) 121 | end 122 | 123 | begin 124 | payment_intent = Stripe::PaymentIntent.create( 125 | :payment_method_types => params[:payment_method_types] || ['card_present'], 126 | :capture_method => params[:capture_method] || 'manual', 127 | :amount => params[:amount], 128 | :currency => params[:currency] || 'usd', 129 | :description => params[:description] || 'Example PaymentIntent', 130 | :payment_method_options => params[:payment_method_options] || [], 131 | :receipt_email => params[:receipt_email], 132 | ) 133 | rescue Stripe::StripeError => e 134 | status 402 135 | return log_info("Error creating PaymentIntent! #{e.message}") 136 | end 137 | 138 | log_info("PaymentIntent successfully created: #{payment_intent.id}") 139 | status 200 140 | return {:intent => payment_intent.id, :secret => payment_intent.client_secret}.to_json 141 | end 142 | 143 | # This endpoint captures a PaymentIntent. 144 | # https://stripe.com/docs/terminal/payments#capture 145 | post '/capture_payment_intent' do 146 | begin 147 | id = params["payment_intent_id"] 148 | if !params["amount_to_capture"].nil? 149 | payment_intent = Stripe::PaymentIntent.capture(id, :amount_to_capture => params["amount_to_capture"]) 150 | else 151 | payment_intent = Stripe::PaymentIntent.capture(id) 152 | end 153 | rescue Stripe::StripeError => e 154 | status 402 155 | return log_info("Error capturing PaymentIntent! #{e.message}") 156 | end 157 | 158 | log_info("PaymentIntent successfully captured: #{id}") 159 | # Optionally reconcile the PaymentIntent with your internal order system. 160 | status 200 161 | return {:intent => payment_intent.id, :secret => payment_intent.client_secret}.to_json 162 | end 163 | 164 | # This endpoint cancels a PaymentIntent. 165 | # https://stripe.com/docs/api/payment_intents/cancel 166 | post '/cancel_payment_intent' do 167 | begin 168 | id = params["payment_intent_id"] 169 | payment_intent = Stripe::PaymentIntent.cancel(id) 170 | rescue Stripe::StripeError => e 171 | status 402 172 | return log_info("Error canceling PaymentIntent! #{e.message}") 173 | end 174 | 175 | log_info("PaymentIntent successfully canceled: #{id}") 176 | # Optionally reconcile the PaymentIntent with your internal order system. 177 | status 200 178 | return {:intent => payment_intent.id, :secret => payment_intent.client_secret}.to_json 179 | end 180 | 181 | # This endpoint creates a SetupIntent. 182 | # https://stripe.com/docs/api/setup_intents/create 183 | post '/create_setup_intent' do 184 | validationError = validateApiKey 185 | if !validationError.nil? 186 | status 400 187 | return log_info(validationError) 188 | end 189 | 190 | begin 191 | setup_intent_params = { 192 | :payment_method_types => params[:payment_method_types] || ['card_present'], 193 | } 194 | 195 | if !params[:customer].nil? 196 | setup_intent_params[:customer] = params[:customer] 197 | end 198 | 199 | if !params[:description].nil? 200 | setup_intent_params[:description] = params[:description] 201 | end 202 | 203 | if !params[:on_behalf_of].nil? 204 | setup_intent_params[:on_behalf_of] = params[:on_behalf_of] 205 | end 206 | 207 | setup_intent = Stripe::SetupIntent.create(setup_intent_params) 208 | 209 | rescue Stripe::StripeError => e 210 | status 402 211 | return log_info("Error creating SetupIntent! #{e.message}") 212 | end 213 | 214 | log_info("SetupIntent successfully created: #{setup_intent.id}") 215 | status 200 216 | return {:intent => setup_intent.id, :secret => setup_intent.client_secret}.to_json 217 | end 218 | 219 | # Looks up or creates a Customer on your stripe account 220 | # with email "example@test.com". 221 | def lookupOrCreateExampleCustomer 222 | customerEmail = "example@test.com" 223 | begin 224 | customerList = Stripe::Customer.list(email: customerEmail, limit: 1).data 225 | if (customerList.length == 1) 226 | return customerList[0] 227 | else 228 | return Stripe::Customer.create(email: customerEmail) 229 | end 230 | rescue Stripe::StripeError => e 231 | status 402 232 | return log_info("Error creating or retreiving customer! #{e.message}") 233 | end 234 | end 235 | 236 | # This endpoint attaches a PaymentMethod to a Customer. 237 | # https://stripe.com/docs/terminal/payments/saving-cards#read-reusable-card 238 | post '/attach_payment_method_to_customer' do 239 | begin 240 | customer = lookupOrCreateExampleCustomer 241 | 242 | payment_method = Stripe::PaymentMethod.attach( 243 | params[:payment_method_id], 244 | { 245 | customer: customer.id, 246 | expand: ["customer"], 247 | }) 248 | rescue Stripe::StripeError => e 249 | status 402 250 | return log_info("Error attaching PaymentMethod to Customer! #{e.message}") 251 | end 252 | 253 | log_info("Attached PaymentMethod to Customer: #{customer.id}") 254 | 255 | status 200 256 | # Note that returning the Stripe payment_method object directly creates a dependency between your 257 | # backend's Stripe.api_version and your clients, making future upgrades more complicated. 258 | # All clients must also be ready for backwards-compatible changes at any time: 259 | # https://stripe.com/docs/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible 260 | return payment_method.to_json 261 | end 262 | 263 | # This endpoint updates the PaymentIntent represented by 'payment_intent_id'. 264 | # It currently only supports updating the 'receipt_email' property. 265 | # 266 | # https://stripe.com/docs/api/payment_intents/update 267 | post '/update_payment_intent' do 268 | payment_intent_id = params["payment_intent_id"] 269 | if payment_intent_id.nil? 270 | status 400 271 | return log_info("'payment_intent_id' is a required parameter") 272 | end 273 | 274 | begin 275 | allowed_keys = ["receipt_email"] 276 | update_params = params.select { |k, _| allowed_keys.include?(k) } 277 | 278 | payment_intent = Stripe::PaymentIntent.update( 279 | payment_intent_id, 280 | update_params 281 | ) 282 | 283 | log_info("Updated PaymentIntent #{payment_intent_id}") 284 | rescue Stripe::StripeError => e 285 | status 402 286 | return log_info("Error updating PaymentIntent #{payment_intent_id}. #{e.message}") 287 | end 288 | 289 | status 200 290 | return {:intent => payment_intent.id, :secret => payment_intent.client_secret}.to_json 291 | end 292 | 293 | # This endpoint lists the first 100 Locations. If you will have more than 100 294 | # Locations, you'll likely want to implement pagination in your application so that 295 | # you can efficiently fetch Locations as needed. 296 | # https://stripe.com/docs/api/terminal/locations 297 | get '/list_locations' do 298 | validationError = validateApiKey 299 | if !validationError.nil? 300 | status 400 301 | return log_info(validationError) 302 | end 303 | 304 | begin 305 | locations = Stripe::Terminal::Location.list( 306 | limit: 100 307 | ) 308 | rescue Stripe::StripeError => e 309 | status 402 310 | return log_info("Error fetching Locations! #{e.message}") 311 | end 312 | 313 | log_info("#{locations.data.size} Locations successfully fetched") 314 | 315 | status 200 316 | content_type :json 317 | return locations.data.to_json 318 | end 319 | 320 | # This endpoint creates a Location. 321 | # https://stripe.com/docs/api/terminal/locations 322 | post '/create_location' do 323 | validationError = validateApiKey 324 | if !validationError.nil? 325 | status 400 326 | return log_info(validationError) 327 | end 328 | 329 | begin 330 | location = Stripe::Terminal::Location.create( 331 | display_name: params[:display_name], 332 | address: params[:address] 333 | ) 334 | rescue Stripe::StripeError => e 335 | status 402 336 | return log_info("Error creating Location! #{e.message}") 337 | end 338 | 339 | log_info("Location successfully created: #{location.id}") 340 | 341 | status 200 342 | content_type :json 343 | return location.to_json 344 | end 345 | --------------------------------------------------------------------------------