31 Commits
Author SHA1 Message Date
Rimian Perkins d857cd7fed add link to T&Cs 2017-06-01 13:50:11 +10:00
Rimian Perkins 93c0b02a45 add price per quarter 2017-05-30 10:12:26 +10:00
Rimian Perkins 52468d17b9 disable button earlier to prevent double clicks 2017-05-27 16:58:21 +10:00
Rimian Perkins 17f5fd2841 remove theme component 2017-05-25 09:15:18 +10:00
Rimian Perkins 255fd7f636 bump: change the plans 2017-05-24 15:26:28 +10:00
Rimian Perkins 824f61aab1 change the plans 2017-05-24 15:26:06 +10:00
Rimian Perkins aff99cb20b bump with hide button 2017-05-24 14:58:56 +10:00
Rimian Perkins ccea023274 add hide donate button 2017-05-24 14:58:20 +10:00
Rimian Perkins 2e91a8e3a3 hide button on success 2017-05-24 14:04:05 +10:00
Rimian Perkins 6ab3de9fca fix success variable 2017-05-24 14:02:14 +10:00
Rimian Perkins 3c431b9915 show success message 2017-05-24 13:55:04 +10:00
Rimian Perkins 1853bdd6f5 Update README.md 2017-05-24 12:46:04 +10:00
Rimian Perkins 8e700c4650 Update README.md 2017-05-24 12:45:35 +10:00
Rimian Perkins 4933ae2233 test more cards 2017-05-24 12:38:54 +10:00
Rimian Perkins 3bdef7f582 more documentation 2017-05-24 12:31:40 +10:00
Rimian Perkins 518cedab1b merge 2017-05-24 12:24:40 +10:00
Rimian Perkins e981471217 change output and test more cards 2017-05-24 12:16:31 +10:00
Rimian Perkins a43f79d7a0 handle some standard stripe fail stuff 2017-05-24 12:03:52 +10:00
Rimian Perkins be122744a3 bump 2017-05-24 11:08:03 +10:00
Rimian Perkins b13cbe663e fix the params 2017-05-24 10:39:57 +10:00
Rimian Perkins f870634afd update success message 2017-05-24 10:34:30 +10:00
Rimian Perkins 6a216d81e5 remove price calc 2017-05-24 10:30:03 +10:00
Rimian Perkins 2f33bdf76b add test fixture for subscriptions 2017-05-24 10:24:41 +10:00
Rimian Perkins 545e962291 buy other plans 2017-05-24 10:16:17 +10:00
Rimian Perkins 904cd4d1d9 check subscription response 2017-05-23 20:14:28 +10:00
Rimian Perkins 49bcd392d4 fix params 2017-05-23 19:41:45 +10:00
Rimian Perkins d531684373 permit create account 2017-05-23 14:36:16 +10:00
Rimian Perkins 377d873f2b fix syntax error 2017-05-23 14:25:25 +10:00
Rimian Perkins f942223eac fix syntax error 2017-05-23 14:23:27 +10:00
Rimian Perkins 83b4e72ff4 add inital plan 2017-05-23 13:54:27 +10:00
Rimian Perkins b80440206b stripe service can create subscriptions 2017-05-23 12:27:34 +10:00
276 changed files with 1298 additions and 14235 deletions
-6
View File
@@ -1,6 +0,0 @@
{
"globals": {
"Stripe": true
},
"extends": "eslint-config-discourse"
}
-57
View File
@@ -1,57 +0,0 @@
name: Linting
on:
push:
branches:
- main
pull_request:
concurrency:
group: plugin-linting-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
- name: Yarn install
run: yarn install
- name: Set up ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
bundler-cache: true
- name: ESLint
if: ${{ always() }}
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts
- name: Prettier
if: ${{ always() }}
shell: bash
run: |
yarn prettier -v
if [ 0 -lt $(find assets -type f \( -name "*.scss" -or -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
yarn prettier --list-different "assets/**/*.{scss,js,es6}"
fi
if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
yarn prettier --list-different "test/**/*.{js,es6}"
fi
- name: Ember template lint
if: ${{ always() }}
run: yarn ember-template-lint --no-error-on-unmatched-pattern assets/javascripts
- name: Rubocop
if: ${{ always() }}
run: bundle exec rubocop .
-156
View File
@@ -1,156 +0,0 @@
name: Plugin Tests
on:
push:
branches:
- main
pull_request:
concurrency:
group: plugin-tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }}
cancel-in-progress: true
jobs:
build:
name: ${{ matrix.build_type }}
runs-on: ubuntu-latest
container: discourse/discourse_test:slim${{ startsWith(matrix.build_type, 'frontend') && '-browsers' || '' }}
timeout-minutes: 30
env:
DISCOURSE_HOSTNAME: www.example.com
RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072
RAILS_ENV: test
PGUSER: discourse
PGPASSWORD: discourse
strategy:
fail-fast: false
matrix:
build_type: ["backend", "frontend-legacy", "frontend"]
steps:
- uses: actions/checkout@v3
with:
repository: discourse/discourse
fetch-depth: 1
- name: Install plugin
uses: actions/checkout@v3
with:
path: plugins/${{ github.event.repository.name }}
fetch-depth: 1
- name: Setup Git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Discourse CI"
- name: Start redis
run: |
redis-server /etc/redis/redis.conf &
- name: Start Postgres
run: |
chown -R postgres /var/run/postgresql
sudo -E -u postgres script/start_test_db.rb
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
- name: Bundler cache
uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Setup gems
run: |
gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock)
bundle config --local path vendor/bundle
bundle config --local deployment true
bundle config --local without development
bundle install --jobs 4
bundle clean
- name: Lint English locale
if: matrix.build_type == 'backend'
run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml"
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install
run: yarn install
- name: Fetch app state cache
uses: actions/cache@v3
id: app-cache
with:
path: tmp/app-cache
key: >-
${{ hashFiles('.github/workflows/tests.yml') }}-
${{ hashFiles('db/**/*', 'plugins/**/db/**/*') }}-
- name: Restore database from cache
if: steps.app-cache.outputs.cache-hit == 'true'
run: psql -f tmp/app-cache/cache.sql postgres
- name: Restore uploads from cache
if: steps.app-cache.outputs.cache-hit == 'true'
run: rm -rf public/uploads && cp -r tmp/app-cache/uploads public/uploads
- name: Create and migrate database
if: steps.app-cache.outputs.cache-hit != 'true'
run: |
bin/rake db:create
bin/rake db:migrate
- name: Dump database for cache
if: steps.app-cache.outputs.cache-hit != 'true'
run: mkdir -p tmp/app-cache && pg_dumpall > tmp/app-cache/cache.sql
- name: Dump uploads for cache
if: steps.app-cache.outputs.cache-hit != 'true'
run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads
- name: Check spec existence
id: check_spec
shell: bash
run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true"
fi
- name: Plugin RSpec
if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exist == 'true'
run: bin/rake plugin:spec[${{ github.event.repository.name }}]
- name: Check qunit existence
id: check_qunit
shell: bash
run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true"
fi
- name: Plugin QUnit
if: matrix.build_type == 'frontend-legacy' && steps.check_qunit.outputs.files_exist == 'true'
run: QUNIT_EMBER_CLI=0 bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000']
timeout-minutes: 10
- name: Plugin QUnit (Ember CLI)
if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exist == 'true'
run: QUNIT_EMBER_CLI=1 bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000']
timeout-minutes: 10
+4 -2
View File
@@ -1,2 +1,4 @@
node_modules
/gems
.DS_Store
gems
log
.byebug_history
-1
View File
@@ -1 +0,0 @@
{}
-8
View File
@@ -1,8 +0,0 @@
inherit_gem:
rubocop-discourse: default.yml
AllCops:
Exclude:
- "gems/**/*"
- "vendor/bundle/**/*"
Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Enabled: false
+1
View File
@@ -0,0 +1 @@
2.3.3
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};
+42
View File
@@ -0,0 +1,42 @@
language: ruby
env:
global:
- DISCOURSE_HOSTNAME=www.example.com
dist: trusty
cache: bundler --path $TRAVIS_BUILD_DIR/discourse/vendor
addons:
postgresql: 9.5
apt:
packages:
- gifsicle
- jpegoptim
- optipng
- jhead
rvm:
- 2.3.3
services:
- redis-server
before_install:
- git clone https://github.com/discourse/discourse.git discourse
- cd discourse
install:
- touch config/stripe.yml
- git clone $TRAVIS_BUILD_DIR plugins/discourse-donations --depth 50 --branch $TRAVIS_BRANCH
before_script:
- gem install bundler
- bundle install
- bundle exec rake db:create db:migrate
script:
- "bundle exec rake plugin:spec[discourse-donations]"
- "MODULE='Acceptance: Discourse Donations' bundle exec rake qunit:test[20000]"
- "MODULE='component:stripe-card' bundle exec rake qunit:test[20000]"
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
source 'https://rubygems.org'
group :development do
gem 'translations-manager', git: 'https://github.com/discourse/translations-manager.git'
gem 'rubocop-discourse'
end
-44
View File
@@ -1,44 +0,0 @@
GIT
remote: https://github.com/discourse/translations-manager.git
revision: a8b225f7fabd3250ba88a4a2eff797693df51192
specs:
translations-manager (0.6)
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
parallel (1.22.1)
parser (3.1.2.0)
ast (~> 2.4.1)
rainbow (3.1.1)
regexp_parser (2.5.0)
rexml (3.2.5)
rubocop (1.30.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.18.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.18.0)
parser (>= 3.1.1.0)
rubocop-discourse (2.5.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.11.1)
rubocop (~> 1.19)
ruby-progressbar (1.11.0)
unicode-display_width (2.1.0)
PLATFORMS
ruby
DEPENDENCIES
rubocop-discourse
translations-manager!
BUNDLED WITH
2.2.19
+58 -3
View File
@@ -1,5 +1,60 @@
# Discourse Subscriptions Plugin
# Discourse Donations
Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance.
[![Build Status](https://travis-ci.org/choiceaustralia/discourse-donations.svg?branch=master)](https://travis-ci.org/choiceaustralia/discourse-donations)
For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818
Accept donations in Discourse! Integrates with [Stripe](https://stripe.com).
## Installation
* Be sure your site is enforcing https.
* Follow the install instructions here: https://meta.discourse.org/t/install-a-plugin/19157
* Add your Stripe public and private keys in settings and set the currency to your local value.
* Add the following script to your page header in a custom theme component: `<script src="https://js.stripe.com/v3/"></script>`
* Enable the plugin and wait for people to donate money.
## Creating new user accounts
**This is an experimental feature.** A user can create a new account if they makes a successful donation. Enable this in settings. When a user is not logged in, they will be asked to enter details for a new user account. This feature doesn't support mandatory custom user fields yet.
## Test Suite
* To run the rails specs, install the plugin and run `bundle exec rake plugin:spec[discourse-donations]` in the discourse root directory.
* To run qunit tests: `MODULE='Acceptance: Discourse Donations' bundle exec rake qunit:test[20000]`.
* To run Component tests: `MODULE='component:stripe-card' bundle exec rake qunit:test[20000]`.
**Note:**
* If you're using a zsh shell, then you probably get this error: `zsh: no matches found ...` and you'll need to escape the square brackets with backslashes.
## TODO
* Test card fails
* Handle custom fields
* Acceptance test in RSpec not qunit.
## Tested Credit Card Numbers
These numbers can be used in test mode to simulate a transaction. For more information see the [Stripe docs](https://stripe.com/docs/testing).
Card numbers in **bold** have beed tested.
* **4000 0000 0000 0077** Charge succeeds and funds will be added directly to your available balance (bypassing your pending balance).
* **4000 0000 0000 0093** Charge succeeds and domestic pricing is used (other test cards use international pricing). This card is only significant in countries with split pricing.
* **4000 0000 0000 0010** The address_line1_check and address_zip_check verifications fail. If your account is blocking payments that fail postal code validation, the charge is declined.
* **4000 0000 0000 0028** Charge succeeds but the address_line1_check verification fails.
* **4000 0000 0000 0036** The address_zip_check verification fails. If your account is blocking payments that fail postal code validation, the charge is declined.
* **4000 0000 0000 0044** Charge succeeds but the address_zip_check and address_line1_check verifications are both unavailable.
* **4000 0000 0000 0101** If a CVC number is provided, the cvc_check fails. If your account is blocking payments that fail CVC code validation, the charge is declined.
* **4000 0000 0000 0341** Attaching this card to a Customer object succeeds, but attempts to charge the customer fail.
* **4000 0000 0000 9235** Charge succeeds with a risk_level of elevated and placed into review.
* **4000 0000 0000 0002** Charge is declined with a card_declined code.
* **4100 0000 0000 0019** Charge is declined with a card_declined code and a fraudulent reason.
* **4000 0000 0000 0127** Charge is declined with an incorrect_cvc code.
* **4000 0000 0000 0069** Charge is declined with an expired_card code.
* **4000 0000 0000 0119** Charge is declined with a processing_error code.
* 4242 4242 4242 4241 Charge is declined with an incorrect_number code as the card number fails the Luhn check.
## Warranty
This software comes with no warranty of any kind.
>>>>>>> master
-11
View File
@@ -1,11 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Group
extend ActiveSupport::Concern
def plan_group(plan)
::Group.find_by_name(plan[:metadata][:group_name])
end
end
end
-15
View File
@@ -1,15 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Stripe
extend ActiveSupport::Concern
def set_api_key
::Stripe.api_key = SiteSetting.discourse_subscriptions_secret_key
end
def is_stripe_configured?
SiteSetting.discourse_subscriptions_public_key.present? && SiteSetting.discourse_subscriptions_secret_key.present?
end
end
end
@@ -0,0 +1,99 @@
require_dependency 'discourse'
module DiscourseDonations
class ChargesController < ActionController::Base
include CurrentUser
skip_before_filter :verify_authenticity_token, only: [:create]
def create
params.permit(:name, :username, :email, :password, :stripeToken, :amount, :plan, :create_account)
output = { 'messages' => [], 'rewards' => [] }
if create_account
if !email.present? || !params[:username].present?
output['messages'] << I18n.t('login.missing_user_field')
end
if params[:password] && params[:password].length > User.max_password_length
output['messages'] << I18n.t('login.password_too_long')
end
if params[:username] && ::User.reserved_username?(params[:username])
output['messages'] << I18n.t('login.reserved_username')
end
end
if output['messages'].present?
render(:json => output.merge(success: false)) and return
end
payment = DiscourseDonations::Stripe.new(secret_key, stripe_options)
begin
if params['amount'].present?
charge = payment.subscribe(email, params.merge(plan: params[:amount]))
else
charge = payment.subscribe(email, params)
end
rescue ::Stripe::CardError => e
err = e.json_body[:error]
output['messages'] << "There was an error (#{err[:type]})."
#output['messages'] << "Error code: #{err[:code]}" if err[:code]
#output['messages'] << "Decline code: #{err[:decline_code]}" if err[:decline_code]
output['messages'] << "Message: #{err[:message]}" if err[:message]
render(:json => output) and return
end
if charge['paid'] == true || charge['status'] == 'active'
output['success'] = true
output['messages'] << I18n.t('donations.payment.success')
output['rewards'] << { type: :group, name: group_name } if group_name
output['rewards'] << { type: :badge, name: badge_name } if badge_name
if create_account && email.present?
args = params.slice(:email, :username, :password, :name).merge(rewards: output['rewards'])
Jobs.enqueue(:donation_user, args)
end
end
render :json => output
end
private
def create_account
params[:create_account] == 'true' && SiteSetting.discourse_donations_enable_create_accounts
end
def reward?(payment)
payment.present? && payment.successful?
end
def group_name
SiteSetting.discourse_donations_reward_group_name
end
def badge_name
SiteSetting.discourse_donations_reward_badge_name
end
def secret_key
SiteSetting.discourse_donations_secret_key
end
def stripe_options
{
description: SiteSetting.discourse_donations_description,
currency: SiteSetting.discourse_donations_currency
}
end
def email
params[:email] || current_user.try(:email)
end
end
end
@@ -0,0 +1,11 @@
module DiscourseDonations
class PaymentsController < ApplicationController
def index
render json: {}
end
def show
render json: {}
end
end
end
@@ -1,72 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Admin
class CouponsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
def index
begin
promo_codes = ::Stripe::PromotionCode.list({ limit: 100 })[:data]
promo_codes = promo_codes.select { |code| code[:coupon][:valid] == true }
render_json_dump promo_codes
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def create
params.require([:promo, :discount_type, :discount, :active])
begin
coupon_params = {
duration: 'forever',
}
case params[:discount_type]
when 'amount'
coupon_params[:amount_off] = params[:discount].to_i * 100
coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency
when 'percent'
coupon_params[:percent_off] = params[:discount]
end
coupon = ::Stripe::Coupon.create(coupon_params)
promo_code = ::Stripe::PromotionCode.create({ coupon: coupon[:id], code: params[:promo] }) if coupon.present?
render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def update
params.require([:id, :active])
begin
promo_code = ::Stripe::PromotionCode.update(
params[:id],
{
active: params[:active]
}
)
render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def destroy
params.require(:coupon_id)
begin
coupon = ::Stripe::Coupon.delete(params[:coupon_id])
render_json_dump coupon
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
end
end
end
@@ -1,97 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Admin
class PlansController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
before_action :set_api_key
def index
begin
plans = ::Stripe::Price.list(product_params)
render_json_dump plans.data
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def create
begin
price_object = {
nickname: params[:nickname],
unit_amount: params[:amount],
product: params[:product],
currency: params[:currency],
active: params[:active],
metadata: {
group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days]
}
}
if params[:type] == 'recurring'
price_object[:recurring] = {
interval: params[:interval]
}
end
plan = ::Stripe::Price.create(price_object)
render_json_dump plan
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def show
begin
plan = ::Stripe::Price.retrieve(params[:id])
if plan[:metadata] && plan[:metadata][:trial_period_days]
trial_days = plan[:metadata][:trial_period_days]
elsif plan[:recurring] && plan[:recurring][:trial_period_days]
trial_days = plan[:recurring][:trial_period_days]
end
interval = nil
if plan[:recurring] && plan[:recurring][:interval]
interval = plan[:recurring][:interval]
end
serialized = plan.to_h.merge(trial_period_days: trial_days, currency: plan[:currency].upcase, interval: interval)
render_json_dump serialized
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def update
begin
plan = ::Stripe::Price.update(
params[:id],
nickname: params[:nickname],
active: params[:active],
metadata: {
group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days]
}
)
render_json_dump plan
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
private
def product_params
{ product: params[:product_id] } if params[:product_id]
end
end
end
end
@@ -1,104 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Admin
class ProductsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
before_action :set_api_key
def index
begin
product_ids = Product.all.pluck(:external_id)
products = []
if product_ids.present? && is_stripe_configured?
products = ::Stripe::Product.list({ ids: product_ids, limit: 100 })
products = products[:data]
elsif !is_stripe_configured?
products = nil
end
render_json_dump products
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def create
begin
create_params = product_params.merge!(type: 'service')
if params[:statement_descriptor].blank?
create_params.except!(:statement_descriptor)
end
product = ::Stripe::Product.create(create_params)
Product.create(
external_id: product[:id]
)
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def show
begin
product = ::Stripe::Product.retrieve(params[:id])
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def update
begin
product = ::Stripe::Product.update(
params[:id],
product_params
)
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def destroy
begin
product = ::Stripe::Product.delete(params[:id])
Product.delete_by(external_id: params[:id])
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
private
def product_params
params.permit!
{
name: params[:name],
active: params[:active],
statement_descriptor: params[:statement_descriptor],
metadata: {
description: params.dig(:metadata, :description),
repurchaseable: params.dig(:metadata, :repurchaseable)
}
}
end
end
end
end
@@ -1,96 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module Admin
class SubscriptionsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
PAGE_LIMIT = 10
def index
begin
subscription_ids = Subscription.all.pluck(:external_id)
subscriptions = {
has_more: false,
data: [],
length: 0,
last_record: params[:last_record]
}
if subscription_ids.present? && is_stripe_configured?
while subscriptions[:length] < PAGE_LIMIT
current_set = get_subscriptions(subscriptions[:last_record])
until valid_subscriptions = find_valid_subscriptions(current_set[:data], subscription_ids) do
current_set = get_subscriptions(current_set[:data].last)
break if current_set[:has_more] == false
end
subscriptions[:data] = subscriptions[:data].concat(valid_subscriptions.to_a)
subscriptions[:last_record] = current_set[:data].last[:id] if current_set[:data].present?
subscriptions[:length] = subscriptions[:data].length
subscriptions[:has_more] = current_set[:has_more]
break if subscriptions[:has_more] == false
end
elsif !is_stripe_configured?
subscriptions = nil
end
render_json_dump subscriptions
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def destroy
params.require(:id)
begin
refund_subscription(params[:id]) if params[:refund]
subscription = ::Stripe::Subscription.delete(params[:id])
customer = Customer.find_by(
product_id: subscription[:plan][:product],
customer_id: subscription[:customer]
)
Subscription.delete_by(external_id: params[:id])
if customer
user = ::User.find(customer.user_id)
customer.delete
group = plan_group(subscription[:plan])
group.remove(user) if group
end
render_json_dump subscription
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
private
def get_subscriptions(start)
::Stripe::Subscription.list(expand: ['data.plan.product'], limit: PAGE_LIMIT, starting_after: start)
end
def find_valid_subscriptions(data, ids)
valid = data.select { |sub| ids.include?(sub[:id]) }
valid.empty? ? nil : valid
end
# this will only refund the most recent subscription payment
def refund_subscription(subscription_id)
subscription = ::Stripe::Subscription.retrieve(subscription_id)
invoice = ::Stripe::Invoice.retrieve(subscription[:latest_invoice]) if subscription[:latest_invoice]
payment_intent = invoice[:payment_intent] if invoice[:payment_intent]
refund = ::Stripe::Refund.create({
payment_intent: payment_intent,
})
end
end
end
end
@@ -1,23 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class AdminController < ::Admin::AdminController
def index
head 200
end
def refresh_campaign
Jobs.enqueue(:manually_update_campaign_data)
render json: success_json
end
def create_campaign
begin
DiscourseSubscriptions::Campaign.new.create_campaign
render json: success_json
rescue => e
render_json_error e.message
end
end
end
end
@@ -1,82 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class HooksController < ::ApplicationController
include DiscourseSubscriptions::Group
include DiscourseSubscriptions::Stripe
layout false
skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required
skip_before_action :verify_authenticity_token, only: [:create]
def create
begin
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
webhook_secret = SiteSetting.discourse_subscriptions_webhook_secret
event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret)
rescue JSON::ParserError => e
render_json_error e.message
return
rescue ::Stripe::SignatureVerificationError => e
render_json_error e.message
return
end
case event[:type]
when 'customer.subscription.updated'
customer = Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product]
)
if customer && subscription_completion?(event)
user = ::User.find(customer.user_id)
group = plan_group(event[:data][:object][:plan])
group.add(user) if group
end
when 'customer.subscription.deleted'
delete_subscription(event)
end
head 200
end
private
def subscription_completion?(event)
subscription_complete?(event) && previously_incomplete?(event)
end
def subscription_complete?(event)
event && event[:data] && event[:data][:object] && event[:data][:object][:status] && event[:data][:object][:status] == 'complete'
end
def previously_incomplete?(event)
event && event[:data] && event[:data][:previous_attributes] && event[:data][:previous_attributes][:status] && event[:data][:previous_attributes][:status] == 'incomplete'
end
def delete_subscription(event)
customer = Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product]
)
if customer
sub_model = Subscription.find_by(
customer_id: customer.id,
external_id: [:id]
)
sub_model.delete if sub_model
user = ::User.find(customer.user_id)
customer.delete
group = plan_group(event[:data][:object][:plan])
group.remove(user) if group
end
end
end
end
@@ -1,215 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class SubscribeController < ::ApplicationController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
requires_login except: [:index, :contributors, :show]
def index
begin
product_ids = Product.all.pluck(:external_id)
products = []
if product_ids.present? && is_stripe_configured?
response = ::Stripe::Product.list({
ids: product_ids,
active: true
})
products = response[:data].map do |p|
serialize_product(p)
end
end
render_json_dump products
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def contributors
return unless SiteSetting.discourse_subscriptions_campaign_show_contributors
contributor_ids = Set.new
campaign_product = SiteSetting.discourse_subscriptions_campaign_product
campaign_product.present? ? contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id)) : contributor_ids.merge(Customer.last(5).pluck(:user_id))
contributors = ::User.where(id: contributor_ids)
render_serialized(contributors, UserSerializer)
end
def show
params.require(:id)
begin
product = ::Stripe::Product.retrieve(params[:id])
plans = ::Stripe::Price.list(active: true, product: params[:id])
response = {
product: serialize_product(product),
plans: serialize_plans(plans)
}
render_json_dump response
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def create
params.require([:source, :plan])
begin
customer = find_or_create_customer(params[:source])
plan = ::Stripe::Price.retrieve(params[:plan])
if params[:promo].present?
promo_code = ::Stripe::PromotionCode.list({ code: params[:promo] })
promo_code = promo_code[:data][0] # we assume promo codes have a unique name
return render_json_error I18n.t("js.discourse_subscriptions.subscribe.invalid_coupon") if promo_code.blank?
end
recurring_plan = plan[:type] == 'recurring'
if recurring_plan
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days]
promo_code_id = promo_code[:id] if promo_code
transaction = ::Stripe::Subscription.create(
customer: customer[:id],
items: [{ price: params[:plan] }],
metadata: metadata_user,
trial_period_days: trial_days,
promotion_code: promo_code_id
)
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete'
else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && promo_code[:coupon][:id]
invoice_item = ::Stripe::InvoiceItem.create(
customer: customer[:id],
price: params[:plan],
discounts: [{ coupon: coupon_id }]
)
invoice = ::Stripe::Invoice.create(
customer: customer[:id]
)
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] == 'open'
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] == 'successful'
end
finalize_transaction(transaction, plan) if transaction_ok(transaction)
transaction = transaction.to_h.merge(transaction, payment_intent: payment_intent)
render_json_dump transaction
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def finalize
params.require([:plan, :transaction])
begin
price = ::Stripe::Price.retrieve(params[:plan])
transaction = retrieve_transaction(params[:transaction])
finalize_transaction(transaction, price) if transaction_ok(transaction)
render_json_dump params[:transaction]
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def finalize_transaction(transaction, plan)
group = plan_group(plan)
group.add(current_user) if group
customer = Customer.create(
user_id: current_user.id,
customer_id: transaction[:customer],
product_id: plan[:product]
)
if transaction[:object] == 'subscription'
Subscription.create(
customer_id: customer.id,
external_id: transaction[:id]
)
end
end
private
def serialize_product(product)
{
id: product[:id],
name: product[:name],
description: PrettyText.cook(product[:metadata][:description]),
subscribed: current_user_products.include?(product[:id]),
repurchaseable: product[:metadata][:repurchaseable]
}
end
def current_user_products
return [] if current_user.nil?
Customer
.select(:product_id)
.where(user_id: current_user.id)
.map { |c| c.product_id }.compact
end
def serialize_plans(plans)
plans[:data].map do |plan|
plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring)
end.sort_by { |plan| plan[:amount] }
end
def find_or_create_customer(source)
customer = Customer.find_by_user_id(current_user.id)
if customer.present?
::Stripe::Customer.retrieve(customer.customer_id)
else
::Stripe::Customer.create(
email: current_user.email,
source: source
)
end
end
def retrieve_payment_intent(invoice_id)
invoice = ::Stripe::Invoice.retrieve(invoice_id)
::Stripe::PaymentIntent.retrieve(invoice[:payment_intent])
end
def retrieve_transaction(transaction)
begin
case transaction
when /^sub_/
::Stripe::Subscription.retrieve(transaction)
when /^in_/
::Stripe::Invoice.retrieve(transaction)
end
rescue ::Stripe::InvalidRequestError => e
e.message
end
end
def metadata_user
{ user_id: current_user.id, username: current_user.username_lower }
end
def transaction_ok(transaction)
%w[active trialing paid].include?(transaction[:status])
end
end
end
@@ -1,56 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module User
class PaymentsController < ::ApplicationController
include DiscourseSubscriptions::Stripe
before_action :set_api_key
requires_login
def index
begin
customer = Customer.where(user_id: current_user.id)
customer_ids = customer.map { |c| c.customer_id } if customer
product_ids = Product.all.pluck(:external_id)
data = []
if customer_ids.present? && product_ids.present?
customer_ids.each do |customer_id|
# lots of matching because the Stripe API doesn't make it easy to match products => payments except from invoices
all_invoices = ::Stripe::Invoice.list(customer: customer_id)
invoices_with_products = parse_invoices(all_invoices, product_ids)
invoice_ids = invoices_with_products.map { |invoice| invoice[:id] }
payments = ::Stripe::PaymentIntent.list(customer: customer_id)
payments_from_invoices = payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
data = data | payments_from_invoices
end
end
data = data.sort_by { |pmt| pmt[:created] }.reverse
render_json_dump data
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
private
def parse_invoices(all_invoices, product_ids)
invoices_with_products = all_invoices[:data].select do |invoice|
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
invoice_product_id = parse_invoice_lines(invoice_lines)
product_ids.include?(invoice_product_id)
end
end
def parse_invoice_lines(invoice_lines)
invoice_product_id = invoice_lines[:price][:product] if invoice_lines[:price] && invoice_lines[:price][:product]
invoice_product_id = invoice_lines[:plan][:product] if invoice_lines[:plan] && invoice_lines[:plan][:product]
invoice_product_id
end
end
end
end
@@ -1,68 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
module User
class SubscriptionsController < ::ApplicationController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
requires_login
def index
begin
customer = Customer.where(user_id: current_user.id)
customer_ids = customer.map { |c| c.id } if customer
subscription_ids = Subscription.where("customer_id in (?)", customer_ids).pluck(:external_id) if customer_ids
subscriptions = []
if subscription_ids
plans = ::Stripe::Price.list(
expand: ['data.product'],
limit: 100
)
customers = ::Stripe::Customer.list(
email: current_user.email,
expand: ['data.subscriptions']
)
subscriptions = customers[:data].map do |sub_customer|
sub_customer[:subscriptions][:data]
end.flatten(1)
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions.map! do |subscription|
plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
subscription.to_h.except!(:plan)
subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name))
end
end
render_json_dump subscriptions
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def destroy
# we cancel but don't remove until the end of the period
# full removal is done via webhooks
begin
subscription = ::Stripe::Subscription.update(params[:id], { cancel_at_period_end: true, })
if subscription
render_json_dump subscription
else
render_json_error I18n.t('discourse_subscriptions.customer_not_found')
end
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
end
end
end
@@ -0,0 +1,5 @@
module DiscourseDonations
module ApplicationHelper
end
end
+1
View File
@@ -0,0 +1 @@
load File.expand_path('../regular/donation_user.rb', __FILE__)
+18
View File
@@ -0,0 +1,18 @@
module Jobs
class DonationUser < ::Jobs::Base
def execute(args)
user = User.create!(args.slice(:username, :password, :name, :email))
return unless user.persisted?
Jobs.enqueue(
:critical_user_email,
type: :signup, user_id: user.id, email_token: user.email_tokens.first.token
)
rewards = DiscourseDonations::Rewards.new(user)
args[:rewards].to_a.each do |reward|
rewards.grant_badge(reward[:name]) if reward[:type] == 'badge'
rewards.add_to_group(reward[:name]) if reward[:type] == 'group'
end
end
end
end
@@ -1,11 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class ManuallyUpdateCampaignData < ::Jobs::Base
def execute(args)
return unless SiteSetting.discourse_subscriptions_campaign_enabled
DiscourseSubscriptions::Campaign.new.refresh_data
end
end
end
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module ::Jobs
class RefreshSubscriptionsCampaignData < ::Jobs::Scheduled
include ::DiscourseSubscriptions::Stripe
every 30.minutes
def execute(args)
return unless SiteSetting.discourse_subscriptions_campaign_enabled && is_stripe_configured?
DiscourseSubscriptions::Campaign.new.refresh_data
end
end
end

Some files were not shown because too many files have changed in this diff Show More