Author SHA1 Message Date
Nat b1f303a1c8 UX: Surface stripe AuthenticationError on the Admin Dashboard 2024-01-04 15:52:24 +08:00
Isaac Janzen 331c8630c3 DEV: Upgrade modal to Component API (#186)
- Upgrade modal to Component API
- Co-locate templates
2023-12-08 15:25:05 -07:00
David Taylor 2ea8d7b6eb DEV: Resolve transitionTo deprecation (#185) 2023-12-01 18:30:36 +00:00
Discourse Translator Bot 316303da5d Update translations (#184) 2023-11-21 14:37:08 +01:00
Discourse Translator Bot 2f9353bc33 Update translations (#183) 2023-11-14 14:30:37 +01:00
Martin Brennan 3608944b9c DEV: Change settings root from plugins: to discourse_subscriptions (#182)
This is so the plugins settings are better categorized in the site settings UI.
2023-11-13 10:23:25 +10:00
Martin BrennanandAlexDev 3aec1a9336 DEV: Metadata update for plugin.rb (#181)
* Update plugin.rb metadata

* Update plugin.rb

---------

Co-authored-by: AlexDev <[email protected]>
2023-11-07 17:29:37 -08:00
Discourse Translator Bot 2f87566366 Update translations (#180) 2023-11-07 21:31:30 +01:00
Kris ac81a7a830 fix typo (#179) 2023-10-24 12:39:32 -04:00
dependabot[bot] a257b386f9 Bump @babel/traverse from 7.20.10 to 7.23.2 (#178)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 12:34:58 -04:00
Natalie Tay c30fea21ef FIX: Clicking on login should show login modal (#177)
* FIX: Clicking on login should show login modal

* Add system test that shows the login modal appears
2023-09-28 14:21:16 +08:00
alex-tee b855ce9bb9 add JPY currency (#176) 2023-09-06 12:14:41 +02:00
Penar Musaraj 1a80f9d52b DEV: Pin plugin for Discourse 3.1 stable (#175) 2023-08-22 13:50:37 -05:00
Discourse Translator Bot c4f8220a55 Update translations (#174) 2023-08-15 21:25:59 +02:00
Penar Musaraj 7140517ec8 DEV: Fix SCSS deprecations (#172)
* DEV: Fix SCSS deprecations

Followup to https://github.com/discourse/discourse-subscriptions/pull/171

Also fixes:

```
    ╷
581 │       hsl(random(360), 100, 50);
    │       ^^^^^^^^^^^^^^^^^^^^^^^^^
    ╵
    /Users/pmusaraj/Projects/discourse/discourse-subscriptions.scss 581:7  root stylesheet
Deprecation Warning: $lightness: Passing a number without unit % (50) is deprecated.
```

* More cleanup
2023-07-19 11:15:34 -04:00
dependabot[bot] 9173190181 Bump word-wrap from 1.2.3 to 1.2.4 (#173)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 01:57:35 +02:00
Penar Musaraj 77cb62bf1f DEV: Fixes SCSS deprecation in console (#171)
Sample warning was:

```
Using / for division outside of calc() is deprecated and will be removed in Dart Sass 2.0.0.

Recommendation: math.div($sidebar-height, 1.2) or calc($sidebar-height / 1.2)

More info and automated migrator: https://sass-lang.com/d/slash-div
```
2023-07-17 16:42:06 -04:00
dependabot[bot] 2a75da65b8 Bump semver from 6.3.0 to 6.3.1 (#170)
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 10:46:20 +02:00
Kris 00e5bc1614 DEV: remove old shadow definition (#168) 2023-06-15 10:18:04 -04:00
Isaac Janzen e365ced531 DEV: Use angle bracket syntax (#167) 2023-05-28 09:32:27 -05:00
Discourse Translator Bot bf41ea9aa7 Update translations (#166) 2023-05-23 21:23:39 +02:00
Discourse Translator Bot 81205cebaf Update translations (#165) 2023-05-16 19:39:51 +02:00
Mark Reeves 49ddc757c9 Make country names translatable (#164) 2023-05-12 08:55:46 -04:00
Discourse Translator Bot d6337e559a Update translations (#163) 2023-05-10 00:48:25 +02:00
Mark Reeves a6a0cd72c8 FIX: A few more syntax fixes for button actions (#162) 2023-05-08 10:37:37 -04:00
Mark Reeves 7bc34bbbb4 FIX: A few more syntax fixes for button actions (#162) 2023-05-08 10:36:11 -04:00
Mark Reeves 803bba7938 FEATURE: Capture cardholder address fields for Stripe customer (#161)
- Adds the following fields to the subscription payment form:
  - Cardholder Name
  - Country
  - Postal Code
  - Address Line 1
  - City
  - State or Province
- Stripe recommends Cardholder Name & Country for verification; Cardholder Name, Country, and State/Province for US/Canada selections are required fields
- All fields are passed to Stripe for verification on submit
- Fields are also captured on the customer record in Stripe, under Billing Details
2023-05-05 13:20:35 -04:00
Selase Krakani 2babb43ffb FEATURE: Skip product listing if only one product is present (#160)
At the moment, paying for a product typically involves first clicking
the subscribe link added to the navigation bar, selecting a product and
then adding card details to to make the purchase.

This change skips the product selection step if the site has only one
product.
2023-05-04 13:52:04 +00:00
Mark Reeves 9aa4a44b2a FIX: Fix syntax for some buttons following recent updates (#159) 2023-05-03 15:50:20 -04:00
Isaac Janzen c06ae37048 DEV: Use angle bracket syntax (#158) 2023-05-02 09:24:28 -05:00
Discourse Translator Bot 45b7eb6e6c Update translations (#155) 2023-02-21 17:13:37 +01:00
Discourse Translator Bot ee9741aefa Update translations (#154) 2023-02-15 10:53:27 +01:00
Rafael dos Santos Silva bb99a9a66a Revert "DEV: Dart Sass deprecation (#150)" (#151)
This reverts commit 1834095b23.
2023-02-07 18:33:31 -03:00
Rafael dos Santos Silva 1834095b23 DEV: Dart Sass deprecation (#150) 2023-02-07 16:25:47 -03:00
Discourse Translator Bot 8dfdca567d Update translations (#149) 2023-01-24 16:31:56 +01:00
Discourse Translator Bot d4f0b9a43c Update translations (#147) 2023-01-10 20:56:53 +01:00
discoursebotanddiscoursebuild 3051bceb8f DEV: Update CI workflows (#148)
Co-authored-by: discoursebuild <[email protected]>
2023-01-10 19:06:12 +00:00
David Taylor 7fb0936e5c DEV: Update eslint-config-discourse, use prettier for hbs (#146) 2023-01-04 13:39:58 +01:00
dependabot[bot] fb072aa851 Bump json5 from 2.2.1 to 2.2.2 (#145)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-29 12:25:51 -05:00
David Taylor 4e1a17c40c DEV: Introduce syntax_tree for ruby formatting (#144) 2022-12-29 13:35:06 +01:00
Discourse Translator Bot cbbc23fd5a Update translations (#143) 2022-12-13 17:15:12 +01:00
Discourse Translator Bot e917c3bca6 Update translations (#142) 2022-11-29 15:39:40 +01:00
discoursebotanddiscoursebuild 425fbc0af1 DEV: Update CI workflows (#141)
Co-authored-by: discoursebuild <[email protected]>
2022-11-07 14:21:36 -06:00
Discourse Translator Bot c0b7aa65d7 Update translations (#139) 2022-11-02 15:14:51 +01:00
Jarek Radosz d59a283fd2 DEV: Fix mocha deprecations (#140)
It now supports strict keyword argument matching by default.
2022-11-02 10:47:35 +01:00
Discourse Translator Bot 1f9907d9e7 Update translations (#138) 2022-10-19 15:19:40 +02:00
Penar Musaraj 550e8347e3 DEV: Refactor bootbox dialogs (#137) 2022-10-17 14:42:40 -04:00
David Taylor c93be7632c DEV: Bump rubocop-discourse (#136) 2022-10-04 09:18:34 +01:00
Bianca Nenciu c00e539304 UX: Make styling consistent with everything else (#134) 2022-10-03 16:14:32 +03:00
Bianca Nenciu a58743e376 FIX: Simplify Stripe webhook handler (#135)
The 'customer.subscription.created' webhooks were not handled at all and
the logic was overcomplicated.
2022-10-03 13:33:50 +03:00
Angus McLeod aba86e5e25 FIX: Promo code placeholder (#133) 2022-09-23 10:37:28 +02:00
Discourse Translator Bot 827f9df444 Update translations (#132) 2022-09-20 15:52:53 +02:00
Penar Musaraj 39b7253c08 DEV: Refactor payment plan buttons (#131) 2022-09-14 18:32:22 -04:00
Blake Erickson a78b92ddcc FEATURE: Add quick access button to user profile dropdown (#130) 2022-08-10 13:05:54 +01:00
Discourse Translator Bot e48b817e8e Update translations (#128) 2022-07-12 17:26:08 +02:00
Faizaan GaganandBlake Erickson 945af4f140 FIX: Allow user to update card details for recurring subscriptions (#123)
* add new route for card update

* create backend route

* update label

* basic functionality working

* ran rubocop

* added rspec tests for functionality

* make payment_method param compulsory

* fixed js linting

* improve client side error handling

* improve server side error handling

* improved update card page UI

* improve button UI for user subscriptions page

* give feedback to user about save status

* remove heading from last column

* fix padding on edit/delete buttons for update table

Co-authored-by: Blake Erickson <[email protected]>
2022-07-05 19:53:27 -06:00
David Taylor d4d92d9653 DEV: Use closure actions (#127) 2022-07-06 00:20:12 +01:00
Faizaan Gagan 392b88265b FIX: set Discourse consistent styles on Stripe card element (#124)
* FIX: set Discourse consistent styles on Stripe card element

* use correct placeholder color

* applied prettier

* mock update method for cardElement

* minor formatting
2022-06-24 21:18:38 +05:30
Blake Erickson 8762d6742e FIX: Import script confirmation flag logic (#126)
The import script has a prompt asking if this is a "procourse" import or
not, but the logic for handling the answer is backwards and will
incorrectly set the `procourse_import` flag to `true`.

This commit also fixes a minor typo.
2022-06-22 17:51:14 -06:00
Discourse Translator Bot 353898a6b4 Update translations (#125) 2022-06-21 15:29:16 +02:00
discoursebotanddiscoursebuild 97caad7205 DEV: Update CI workflows (#122)
Co-authored-by: discoursebuild <[email protected]>
2022-06-17 22:22:31 +02:00
174 changed files with 8667 additions and 2382 deletions
+2
View File
@@ -0,0 +1,2 @@
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
+11
View File
@@ -0,0 +1,11 @@
name: Discourse Plugin
on:
push:
branches:
- main
pull_request:
jobs:
ci:
uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1
-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
+1 -1
View File
@@ -1,5 +1,5 @@
inherit_gem:
rubocop-discourse: default.yml
rubocop-discourse: stree-compat.yml
AllCops:
Exclude:
- "gems/**/*"
+2
View File
@@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma
+4 -3
View File
@@ -1,8 +1,9 @@
# frozen_string_literal: true
source 'https://rubygems.org'
source "https://rubygems.org"
group :development do
gem 'translations-manager', git: 'https://github.com/discourse/translations-manager.git'
gem 'rubocop-discourse'
gem "translations-manager", git: "https://github.com/discourse/translations-manager.git"
gem "rubocop-discourse"
gem "syntax_tree"
end
+17 -11
View File
@@ -8,37 +8,43 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.2)
parallel (1.22.1)
parser (3.1.2.0)
parser (3.1.2.1)
ast (~> 2.4.1)
prettier_print (1.2.0)
rainbow (3.1.1)
regexp_parser (2.5.0)
regexp_parser (2.6.0)
rexml (3.2.5)
rubocop (1.30.1)
rubocop (1.36.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.0.0)
parser (>= 3.1.2.1)
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)
rubocop-ast (>= 1.20.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.18.0)
rubocop-ast (1.21.0)
parser (>= 3.1.1.0)
rubocop-discourse (2.5.0)
rubocop-discourse (3.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.11.1)
rubocop (~> 1.19)
rubocop-rspec (2.13.2)
rubocop (~> 1.33)
ruby-progressbar (1.11.0)
unicode-display_width (2.1.0)
syntax_tree (5.1.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
rubocop-discourse
syntax_tree
translations-manager!
BUNDLED WITH
2.2.19
2.3.4
+4 -1
View File
@@ -4,12 +4,15 @@ module DiscourseSubscriptions
module Stripe
extend ActiveSupport::Concern
AUTH_PROBLEM_IDENTIFIER = "#{DiscourseSubscriptions::PLUGIN_NAME}.stripe_auth_error"
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?
SiteSetting.discourse_subscriptions_public_key.present? &&
SiteSetting.discourse_subscriptions_secret_key.present?
end
end
end
@@ -18,23 +18,24 @@ module DiscourseSubscriptions
end
def create
params.require([:promo, :discount_type, :discount, :active])
params.require(%i[promo discount_type discount active])
begin
coupon_params = {
duration: 'forever',
}
coupon_params = { duration: "forever" }
case params[:discount_type]
when 'amount'
when "amount"
coupon_params[:amount_off] = params[:discount].to_i * 100
coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency
when 'percent'
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?
promo_code =
::Stripe::PromotionCode.create(
{ coupon: coupon[:id], code: params[:promo] },
) if coupon.present?
render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e
@@ -43,14 +44,9 @@ module DiscourseSubscriptions
end
def update
params.require([:id, :active])
params.require(%i[id active])
begin
promo_code = ::Stripe::PromotionCode.update(
params[:id],
{
active: params[:active]
}
)
promo_code = ::Stripe::PromotionCode.update(params[:id], { active: params[:active] })
render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e
@@ -27,15 +27,11 @@ module DiscourseSubscriptions
active: params[:active],
metadata: {
group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days]
}
trial_period_days: params[:trial_period_days],
},
}
if params[:type] == 'recurring'
price_object[:recurring] = {
interval: params[:interval]
}
end
price_object[:recurring] = { interval: params[:interval] } if params[:type] == "recurring"
plan = ::Stripe::Price.create(price_object)
@@ -56,14 +52,16 @@ module DiscourseSubscriptions
end
interval = nil
if plan[:recurring] && plan[:recurring][:interval]
interval = plan[:recurring][:interval]
end
interval = plan[:recurring][:interval] if plan[:recurring] && plan[:recurring][:interval]
serialized = plan.to_h.merge(trial_period_days: trial_days, currency: plan[:currency].upcase, interval: interval)
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
@@ -71,15 +69,16 @@ module DiscourseSubscriptions
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]
}
)
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
@@ -27,20 +27,15 @@ module DiscourseSubscriptions
def create
begin
create_params = product_params.merge!(type: 'service')
create_params = product_params.merge!(type: "service")
if params[:statement_descriptor].blank?
create_params.except!(:statement_descriptor)
end
create_params.except!(:statement_descriptor) if params[:statement_descriptor].blank?
product = ::Stripe::Product.create(create_params)
Product.create(
external_id: product[:id]
)
Product.create(external_id: product[:id])
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -51,7 +46,6 @@ module DiscourseSubscriptions
product = ::Stripe::Product.retrieve(params[:id])
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -59,13 +53,9 @@ module DiscourseSubscriptions
def update
begin
product = ::Stripe::Product.update(
params[:id],
product_params
)
product = ::Stripe::Product.update(params[:id], product_params)
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -78,7 +68,6 @@ module DiscourseSubscriptions
Product.delete_by(external_id: params[:id])
render_json_dump product
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -95,8 +84,8 @@ module DiscourseSubscriptions
statement_descriptor: params[:statement_descriptor],
metadata: {
description: params.dig(:metadata, :description),
repurchaseable: params.dig(:metadata, :repurchaseable)
}
repurchaseable: params.dig(:metadata, :repurchaseable),
},
}
end
end
@@ -16,20 +16,23 @@ module DiscourseSubscriptions
has_more: false,
data: [],
length: 0,
last_record: params[:last_record]
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
until valid_subscriptions =
find_valid_subscriptions(current_set[:data], subscription_ids)
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[: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
@@ -50,10 +53,11 @@ module DiscourseSubscriptions
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]
)
customer =
Customer.find_by(
product_id: subscription[:plan][:product],
customer_id: subscription[:customer],
)
Subscription.delete_by(external_id: params[:id])
@@ -65,7 +69,6 @@ module DiscourseSubscriptions
end
render_json_dump subscription
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -74,7 +77,11 @@ module DiscourseSubscriptions
private
def get_subscriptions(start)
::Stripe::Subscription.list(expand: ['data.plan.product'], limit: PAGE_LIMIT, starting_after: start)
::Stripe::Subscription.list(
expand: ["data.plan.product"],
limit: PAGE_LIMIT,
starting_after: start,
)
end
def find_valid_subscriptions(data, ids)
@@ -85,11 +92,11 @@ module DiscourseSubscriptions
# 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]
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,
})
refund = ::Stripe::Refund.create({ payment_intent: payment_intent })
end
end
end
@@ -4,7 +4,9 @@ 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]
@@ -12,71 +14,59 @@ module DiscourseSubscriptions
def create
begin
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
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
return render_json_error e.message
rescue ::Stripe::SignatureVerificationError => e
render_json_error e.message
return
return render_json_error e.message
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]
)
when "customer.subscription.created"
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
return render_json_error "customer not found" if !customer
return head 200 if event[:data][:object][:status] != "complete"
user = ::User.find_by(id: customer.user_id)
return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan])
group.add(user)
end
when "customer.subscription.deleted"
customer =
Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product],
)
return render_json_error "customer not found" if !customer
Subscription.find_by(
customer_id: customer.id,
external_id: event[:data][:object][:id],
)&.destroy!
user = ::User.find(customer.user_id)
return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan])
group.remove(user)
end
when 'customer.subscription.deleted'
delete_subscription(event)
customer.destroy!
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
@@ -5,7 +5,7 @@ module DiscourseSubscriptions
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
requires_login except: [:index, :contributors, :show]
requires_login except: %i[index contributors show]
def index
begin
@@ -13,19 +13,12 @@ module DiscourseSubscriptions
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
response = ::Stripe::Product.list({ ids: product_ids, active: true })
products = response[:data].map { |p| serialize_product(p) }
end
render_json_dump products
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -36,7 +29,11 @@ module DiscourseSubscriptions
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))
if campaign_product.present?
contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id))
else
contributor_ids.merge(Customer.last(5).pluck(:user_id))
end
contributors = ::User.where(id: contributor_ids)
@@ -49,10 +46,7 @@ module DiscourseSubscriptions
product = ::Stripe::Product.retrieve(params[:id])
plans = ::Stripe::Price.list(active: true, product: params[:id])
response = {
product: serialize_product(product),
plans: serialize_plans(plans)
}
response = { product: serialize_product(product), plans: serialize_plans(plans) }
render_json_dump response
rescue ::Stripe::InvalidRequestError => e
@@ -61,47 +55,60 @@ module DiscourseSubscriptions
end
def create
params.require([:source, :plan])
params.require(%i[source plan])
begin
customer = find_or_create_customer(params[:source])
customer =
find_or_create_customer(
params[:source],
params[:cardholder_name],
params[:cardholder_address],
)
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?
if promo_code.blank?
return render_json_error I18n.t("js.discourse_subscriptions.subscribe.invalid_coupon")
end
end
recurring_plan = plan[:type] == 'recurring'
recurring_plan = plan[:type] == "recurring"
if recurring_plan
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days]
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
)
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'
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]
)
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'
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)
@@ -115,7 +122,7 @@ module DiscourseSubscriptions
end
def finalize
params.require([:plan, :transaction])
params.require(%i[plan transaction])
begin
price = ::Stripe::Price.retrieve(params[:plan])
transaction = retrieve_transaction(params[:transaction])
@@ -132,17 +139,15 @@ module DiscourseSubscriptions
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]
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
@@ -154,34 +159,47 @@ module DiscourseSubscriptions
name: product[:name],
description: PrettyText.cook(product[:metadata][:description]),
subscribed: current_user_products.include?(product[:id]),
repurchaseable: product[:metadata][:repurchaseable]
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
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] }
plans[:data]
.map { |plan| plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring) }
.sort_by { |plan| plan[:amount] }
end
def find_or_create_customer(source)
def find_or_create_customer(source, cardholder_name = nil, cardholder_address = nil)
customer = Customer.find_by_user_id(current_user.id)
cardholder_address =
(
if cardholder_address.present?
{
line1: cardholder_address[:line1],
city: cardholder_address[:city],
state: cardholder_address[:state],
country: cardholder_address[:country],
postal_code: cardholder_address[:postalCode],
}
else
nil
end
)
if customer.present?
::Stripe::Customer.retrieve(customer.customer_id)
else
::Stripe::Customer.create(
email: current_user.email,
source: source
source: source,
name: cardholder_name,
address: cardholder_address,
)
end
end
@@ -22,7 +22,8 @@ module DiscourseSubscriptions
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]) }
payments_from_invoices =
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
data = data | payments_from_invoices
end
end
@@ -30,7 +31,6 @@ module DiscourseSubscriptions
data = data.sort_by { |pmt| pmt[:created] }.reverse
render_json_dump data
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -39,16 +39,19 @@ module DiscourseSubscriptions
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
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 = 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
@@ -12,24 +12,21 @@ module DiscourseSubscriptions
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
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
)
plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
customers = ::Stripe::Customer.list(
email: current_user.email,
expand: ['data.subscriptions']
)
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 =
customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1)
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
@@ -41,7 +38,6 @@ module DiscourseSubscriptions
end
render_json_dump subscriptions
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -51,18 +47,41 @@ module DiscourseSubscriptions
# 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, })
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')
render_json_error I18n.t("discourse_subscriptions.customer_not_found")
end
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def update
params.require(:payment_method)
subscription = Subscription.where(external_id: params[:id]).first
begin
attach_method_to_customer(subscription.customer_id, params[:payment_method])
subscription =
::Stripe::Subscription.update(
params[:id],
{ default_payment_method: params[:payment_method] },
)
render json: success_json
rescue ::Stripe::InvalidRequestError
render_json_error I18n.t("discourse_subscriptions.card.invalid")
end
end
private
def attach_method_to_customer(customer_id, method)
customer = Customer.find(customer_id)
::Stripe::PaymentMethod.attach(method, { customer: customer.customer_id })
end
end
end
end
@@ -2,7 +2,6 @@
module ::Jobs
class ManuallyUpdateCampaignData < ::Jobs::Base
def execute(args)
return unless SiteSetting.discourse_subscriptions_campaign_enabled
DiscourseSubscriptions::Campaign.new.refresh_data
@@ -2,14 +2,14 @@
module DiscourseSubscriptions
class PaymentSerializer < ApplicationSerializer
attributes :payment_intent_id,
:receipt_email,
:url,
:created_at_age,
:amount,
:amount_currency,
:username,
:user_id
attributes :payment_intent_id,
:receipt_email,
:url,
:created_at_age,
:amount,
:amount_currency,
:username,
:user_id
def created_at_age
Time.now - object.created_at
@@ -19,7 +19,7 @@ module DiscourseSubscriptions
ActiveSupport::NumberHelper.number_to_currency(
object.amount / 100,
precision: 2,
unit: currency_unit
unit: currency_unit,
)
end
@@ -32,7 +32,7 @@ module DiscourseSubscriptions
def user
begin
User.find(object.user_id)
rescue
rescue StandardError
nil
end
end
@@ -17,28 +17,39 @@ module DiscourseSubscriptions
product_ids = product_ids.include?(campaign_product) ? [campaign_product] : []
end
amount = 0.00
subscriptions = get_subscription_data
subscriptions = filter_to_subscriptions_products(subscriptions, product_ids)
begin
amount = 0.00
subscriptions = get_subscription_data
subscriptions = filter_to_subscriptions_products(subscriptions, product_ids)
# Fetch product purchases
one_time_payments = get_one_time_payments(product_ids)
one_time_payments.each do |c|
amount += c[:price].to_f / 100.00
# Fetch product purchases
one_time_payments = get_one_time_payments(product_ids)
one_time_payments.each { |c| amount += c[:price].to_f / 100.00 }
# get number of subscribers
SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i
# calculate amount raised
subscriptions&.each do |sub|
sub_amount = calculate_monthly_amount(sub)
amount += sub_amount / 100.00
end
SiteSetting.discourse_subscriptions_campaign_amount_raised = amount.round(2)
check_goal_status
rescue ::Stripe::AuthenticationError
message =
I18n.t(
"discourse_subscriptions.auth.invalid",
settingsUrl: "admin/site_settings/category/discourse_subscriptions",
)
AdminDashboardData.add_found_scheduled_check_problem(
AdminDashboardData::Problem.new(message, identifier: AUTH_PROBLEM_IDENTIFIER),
)
else
AdminDashboardData.clear_found_problem(AUTH_PROBLEM_IDENTIFIER)
end
# get number of subscribers
SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i
# calculate amount raised
subscriptions&.each do |sub|
sub_amount = calculate_monthly_amount(sub)
amount += sub_amount / 100.00
end
SiteSetting.discourse_subscriptions_campaign_amount_raised = amount.round(2)
check_goal_status
end
def create_campaign
@@ -57,7 +68,7 @@ module DiscourseSubscriptions
protected
def goal_met_date_key
'subscriptions_goal_met_date'
"subscriptions_goal_met_date"
end
def check_goal_status
@@ -91,9 +102,9 @@ module DiscourseSubscriptions
SiteSetting.discourse_subscriptions_campaign_group = group[:id]
params = {
full_name: I18n.t('js.discourse_subscriptions.campaign.supporters'),
title: I18n.t('js.discourse_subscriptions.campaign.supporter'),
flair_icon: "donate"
full_name: I18n.t("js.discourse_subscriptions.campaign.supporters"),
title: I18n.t("js.discourse_subscriptions.campaign.supporter"),
flair_icon: "donate",
}
group.update(params)
@@ -104,11 +115,11 @@ module DiscourseSubscriptions
def create_campaign_product
product_params = {
name: I18n.t('js.discourse_subscriptions.campaign.title'),
name: I18n.t("js.discourse_subscriptions.campaign.title"),
active: true,
metadata: {
description: I18n.t('js.discourse_subscriptions.campaign.body'),
}
description: I18n.t("js.discourse_subscriptions.campaign.body"),
},
}
product = ::Stripe::Product.create(product_params)
@@ -123,13 +134,9 @@ module DiscourseSubscriptions
monthly_prices = [3, 5, 10, 25]
yearly_prices = [50, 100]
monthly_prices.each do |price|
create_price(product[:id], group, price, "month")
end
monthly_prices.each { |price| create_price(product[:id], group, price, "month") }
yearly_prices.each do |price|
create_price(product[:id], group, price, "year")
end
yearly_prices.each { |price| create_price(product[:id], group, price, "year") }
end
def create_price(product_id, group_name, amount, recurrence)
@@ -140,11 +147,11 @@ module DiscourseSubscriptions
currency: SiteSetting.discourse_subscriptions_currency,
active: true,
recurring: {
interval: recurrence
interval: recurrence,
},
metadata: {
group_name: group_name
}
group_name: group_name,
},
}
plan = ::Stripe::Price.create(price_object)
@@ -152,18 +159,13 @@ module DiscourseSubscriptions
def get_one_time_payments(product_ids)
one_time_payments = []
current_set = {
has_more: true,
last_record: nil
}
current_set = { has_more: true, last_record: nil }
if product_ids.present?
# lots of matching because the Stripe API doesn't make it easy to match products => payments except from invoices
until current_set[:has_more] == false
all_invoices = ::Stripe::Invoice.list(
limit: 100,
starting_after: current_set[:last_record]
)
all_invoices =
::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present?
current_set[:has_more] = all_invoices[:has_more]
@@ -173,11 +175,8 @@ module DiscourseSubscriptions
next if invoice[:paid] != true
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
# check if non-subscription and that the plan is active
if line_item[:plan] == nil &&
line_item[:price] &&
line_item[:price][:recurring] == nil &&
line_item[:price][:active] == true
if line_item[:plan] == nil && line_item[:price] &&
line_item[:price][:recurring] == nil && line_item[:price][:active] == true
product_id = line_item[:price][:product]
if product_ids.include? product_id
line_data = {
@@ -197,17 +196,15 @@ module DiscourseSubscriptions
def get_subscription_data
subscriptions = []
current_set = {
has_more: true,
last_record: nil
}
current_set = { has_more: true, last_record: nil }
until current_set[:has_more] == false
current_set = ::Stripe::Subscription.list(
expand: ['data.plan.product'],
limit: 100,
starting_after: current_set[:last_record]
)
current_set =
::Stripe::Subscription.list(
expand: ["data.plan.product"],
limit: 100,
starting_after: current_set[:last_record],
)
current_set[:last_record] = current_set[:data].last[:id] if current_set[:data].present?
subscriptions.concat(current_set[:data].to_a)
@@ -217,13 +214,14 @@ module DiscourseSubscriptions
end
def filter_to_subscriptions_products(data, ids)
valid = data.select do |sub|
# cannot .dig stripe objects
items = sub[:items][:data][0] if sub[:items] && sub[:items][:data]
product = items[:price][:product] if items[:price] && items[:price][:product]
valid =
data.select do |sub|
# cannot .dig stripe objects
items = sub[:items][:data][0] if sub[:items] && sub[:items][:data]
product = items[:price][:product] if items[:price] && items[:price][:product]
ids.include?(product)
end
ids.include?(product)
end
valid.empty? ? nil : valid
end
@@ -3,7 +3,7 @@
class="campaign-banner"
style={{html-safe (concat "box-shadow: 5px 5px #" dropShadowColor)}}
>
{{d-button icon="times" action="dismissBanner" class="close"}}
<DButton @icon="times" @action={{this.dismissBanner}} class="close" />
<div class="campaign-banner-info" style={{html-safe this.bannerInfoStyle}}>
{{#if isGoalMet}}
@@ -24,25 +24,25 @@
</p>
{{#if product}}
{{#link-to
"subscribe.show"
product
disabled=product.subscribed
<LinkTo
@route="subscribe.show"
@model={{product}}
@disabled={{product.subscribed}}
class="btn btn-primary campaign-banner-info-button"
}}
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
{{/link-to}}
</LinkTo>
{{else}}
{{#link-to
"subscribe"
<LinkTo
@route="subscribe"
class="btn btn-primary campaign-banner-info-button"
}}
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
{{/link-to}}
</LinkTo>
{{/if}}
{{/if}}
</div>
@@ -80,7 +80,7 @@
</p>
{{#if showContributors}}
{{#conditional-loading-spinner condition=loading size="small"}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
@@ -102,7 +102,7 @@
{{/each}}
</div>
</div>
{{/conditional-loading-spinner}}
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
{{else}}
@@ -143,7 +143,7 @@
{{/if}}
{{#if showContributors}}
{{#conditional-loading-spinner condition=loading size="small"}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
@@ -165,9 +165,9 @@
{{/each}}
</div>
</div>
{{/conditional-loading-spinner}}
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
</div>
</div>
{{/if}}
{{/if}}
@@ -0,0 +1,50 @@
<div class="create-coupon-form">
<form class="form-horizontal">
<p>
<label for="promo_code">
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
</label>
<Input @type="text" name="promo_code" @value={{promoCode}} />
</p>
<p>
<label for="amount">
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
</label>
<ComboBox
@content={{discountTypes}}
@value={{discountType}}
@onChange={{action (mut discountType)}}
/>
<Input
class="discount-amount"
@type="text"
name="amount"
@value={{discount}}
/>
</p>
<p>
<label for="active">
{{i18n "discourse_subscriptions.admin.coupons.active"}}
</label>
<Input @type="checkbox" name="active" @checked={{active}} />
</p>
</form>
<DButton
@action={{action "createNewCoupon"}}
@label="discourse_subscriptions.admin.coupons.create"
@title="discourse_subscriptions.admin.coupons.create"
@icon="plus"
class="btn-primary btn btn-icon"
/>
<DButton
@action={{action "cancelCreate"}}
label="cancel"
@title="cancel"
@icon="times"
class="btn btn-icon"
/>
</div>
@@ -1,12 +0,0 @@
import Button from "discourse/components/d-button";
export default Button.extend({
seleted: false,
init() {
this._super(...arguments);
this.classNameBindings = this.classNameBindings.concat(
"selected:btn-primary"
);
},
});
@@ -0,0 +1,8 @@
<h3>{{i18n "discourse_subscriptions.subscribe.unauthenticated"}}</h3>
<DButton
@label="log_in"
@action={{route-action "showLogin"}}
@icon="user"
class="btn btn-primary login-required subscriptions"
/>
@@ -1,17 +0,0 @@
import Component from "@ember/component";
import cookie from "discourse/lib/cookie";
import DiscourseURL from "discourse/lib/url";
import { default as getURL } from "discourse-common/lib/get-url";
export default Component.extend({
classNames: ["login-required", "subscriptions"],
actions: {
createAccount() {
const destinationUrl = window.location.href;
const cookiePath = getURL("/");
cookie("destination_url", destinationUrl, { path: cookiePath });
DiscourseURL.redirectTo("/login");
},
},
});
@@ -0,0 +1,38 @@
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import Component from "@glimmer/component";
import { fn, hash } from "@ember/helper";
import i18n from "discourse-common/helpers/i18n";
import { Input } from "@ember/component";
import { tracked } from "@glimmer/tracking";
export default class AdminCancelSubscription extends Component {
@tracked refund;
<template>
<DModal
@title={{i18n
"discourse_subscriptions.user.subscriptions.operations.destroy.confirm"
}}
@closeModal={{@closeModal}}
>
<:body>
<Input @type="checkbox" @checked={{this.refund}} />
{{i18n "discourse_subscriptions.admin.ask_refund"}}
</:body>
<:footer>
<DButton
@label="yes_value"
@action={{fn
@model.cancelSubscription
(hash [email protected] refund=this.refund)
}}
@icon="times"
@isLoading={{@model.subscription.loading}}
class="btn-danger"
/>
<DButton @label="no_value" @action={{@closeModal}} />
</:footer>
</DModal>
</template>
}
@@ -4,10 +4,10 @@
<div class="subscribe-buttons">
{{#each orderedPlans as |plan|}}
{{payment-plan
plan=plan
selectedPlan=selectedPlan
clickPlan=(action "clickPlan")
}}
<PaymentPlan
@plan={{plan}}
@selectedPlan={{selectedPlan}}
@clickPlan={{action "clickPlan"}}
/>
{{/each}}
</div>
</div>
@@ -0,0 +1,21 @@
<DButton
@action={{action "planClick"}}
class={{concat-class "btn-discourse-subscriptions-subscribe" selectedClass}}
>
<span class="interval">
{{#if this.recurringPlan}}
{{i18n
(concat
"discourse_subscriptions.plans.interval.adverb."
this.plan.recurring.interval
)
}}
{{else}}
{{i18n "discourse_subscriptions.one_time_payment"}}
{{/if}}
</span>
<span class="amount">
{{format-currency this.plan.currency this.plan.amountDollars}}
</span>
</DButton>
@@ -4,9 +4,11 @@ import discourseComputed from "discourse-common/utils/decorators";
const RECURRING = "recurring";
export default Component.extend({
tagName: "",
@discourseComputed("selectedPlan")
selected(planId) {
return planId === this.plan.id;
selectedClass(planId) {
return planId === this.plan.id ? "btn-primary" : "";
},
@discourseComputed("plan.type")
@@ -7,18 +7,22 @@
{{#if isLoggedIn}}
<div class="product-purchase">
{{#if product.repurchaseable}}
{{#link-to "subscribe.show" product.id class="btn btn-primary"}}
<LinkTo
@route="subscribe.show"
@model={{product.id}}
class="btn btn-primary"
>
{{i18n "discourse_subscriptions.subscribe.title"}}
{{/link-to}}
</LinkTo>
{{#if product.subscribed}}
{{#link-to
"user.billing.subscriptions"
currentUser.username
<LinkTo
@route="user.billing.subscriptions"
@model={{currentUser.username}}
class="billing-link"
}}
>
{{i18n "discourse_subscriptions.subscribe.view_past"}}
{{/link-to}}
</LinkTo>
{{/if}}
{{else}}
{{#if product.subscribed}}
@@ -27,23 +31,23 @@
{{i18n "discourse_subscriptions.subscribe.purchased"}}
</span>
{{#link-to
"user.billing.subscriptions"
currentUser.username
<LinkTo
@route="user.billing.subscriptions"
@model={{currentUser.username}}
class="billing-link"
}}
>
{{i18n "discourse_subscriptions.subscribe.go_to_billing"}}
{{/link-to}}
</LinkTo>
{{else}}
{{#link-to
"subscribe.show"
product.id
disabled=product.subscribed
<LinkTo
@route="subscribe.show"
@model={{product.id}}
@disabled={{product.subscribed}}
class="btn btn-primary"
}}
>
{{i18n "discourse_subscriptions.subscribe.title"}}
{{/link-to}}
</LinkTo>
{{/if}}
{{/if}}
</div>
{{/if}}
{{/if}}

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