Author SHA1 Message Date
Blake Erickson 7fe7be7ef8 add nonce to pricing-table.js 2024-04-24 11:38:56 -06:00
Blake Erickson d6c4ec21ae add pricing table to csp 2024-04-24 11:18:47 -06:00
Blake Erickson c48cbc7071 fix spec still linking to /s 2024-04-24 11:12:48 -06:00
spirobel 66cf05943c DEV: Change add_admin_route from products to plans 2024-04-24 11:11:38 -06:00
Blake Erickson e46693b94e linting 2024-04-24 10:09:53 -06:00
spirobel fcc414dbbe DEV: fix backend tests
After the engine url got moved to /subscriptions, the tests had to be adapted.

The subscribe controller does not return products anymore after moving to the stripe pricing table, so the respective tests were removed.
2024-04-23 16:24:34 -06:00
spirobel 411ac6ec80 DEV: add frontend tests to make sure pricing-table works
We test three conditions:
1. Make sure the customer email is prefilled, if there is a logged in discourse user
2. The pricing table also displays, if there is no logged in discourse user
3. If the pricing table is not configured, the pricing table element does not show up.
2024-04-23 15:32:19 -06:00
spirobel ce988c9512 DEV: continue to use subscribe_controller instead of introducting a new controller 2024-04-23 15:30:28 -06:00
spirobel bf7079928e DEV: fix linter issues 2024-04-23 15:26:52 -06:00
spirobel ff981f76e8 DEV: Remove the old product / pricing table display routes 2024-04-23 15:24:58 -06:00
spirobel 13d23dc3fa FEATURE: Add new stripe based pricing table
This commit adds the new stripe based pricing table.
If the user is logged in, the email field will be prepopulated with the users email.

The pricing table can be configured in the stripe dashboard.

Once the discourse_subscriptions_pricing_table setting is filled with the pricing table embed code from the stripe dashboard,
the pricing table will be displayed on /subscriptions

For more details see https://stripe.com/docs/payments/checkout/pricing-table
2024-04-23 15:18:31 -06:00
Blake Erickson dcde03d7c4 DEV: Bump stripe gem (#200)
Upgrading to the latest stripe gem and api version.
2024-04-16 11:28:07 -06:00
Michelle Bueno Saquetim Vendrame d63f241e0d FEATURE: add ZAR currency support (#199) 2024-04-15 21:26:26 +02:00
Discourse Translator Bot 8f1b332f9e Update translations (#198) 2024-04-02 18:20:05 +02:00
Blake Erickson 9190001a3c FIX: Translation missing for Norway (#196)
Turns out in YAML `NO` is interpreted as a boolean value, so we need to
wrap it in quotes.
2024-03-27 15:50:15 -06:00
Natalie Tay 0d28c9a56d FIX: Ensure deletion of product upon confirmation (#195)
Ensure deletion of product upon confirmation
2024-03-13 17:16:07 +08:00
David Taylor 7361721ec0 DEV: Update for strict-dynamic CSP (#194) 2024-03-06 16:45:55 +00:00
Loïc Guitaut 87659e237b DEV: Fix new Rubocop offenses 2024-03-06 15:23:55 +01:00
Selase Krakani 7b8a41ad0d DEV: Pin plugin for Discourse < 3.3.0.beta1-dev (#192) 2024-02-29 13:59:04 -07:00
Discourse Translator Bot 0dcf17de59 Update translations (#191) 2024-01-30 17:04:52 +01:00
Discourse Translator Bot 2cfded9a77 Update translations (#190) 2024-01-23 18:30:34 +01:00
Jarek Radosz d33cf2921e DEV: Update linting (#189) 2024-01-16 17:51:44 +01:00
David Taylor a3883d2a9a DEV: Resolve registerUnbound deprecation (#188) 2024-01-15 11:52:19 +00: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
208 changed files with 10023 additions and 3421 deletions
+3
View File
@@ -0,0 +1,3 @@
< 3.3.0.beta1-dev: 0dcf17de59bdf82c0233bb630dad0ce407d1ae76
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
-6
View File
@@ -1,6 +0,0 @@
{
"globals": {
"Stripe": true
},
"extends": "eslint-config-discourse"
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/eslint");
+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
View File
@@ -1 +0,0 @@
{}
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/prettier");
+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
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/template-lint");
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};
+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
+59 -18
View File
@@ -7,38 +7,79 @@ GIT
GEM
remote: https://rubygems.org/
specs:
activesupport (7.1.3.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2)
parallel (1.22.1)
parser (3.1.2.0)
base64 (0.2.0)
bigdecimal (3.1.6)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
drb (2.2.1)
i18n (1.14.3)
concurrent-ruby (~> 1.0)
racc (~> 1.7)
json (2.7.1)
language_server-protocol (3.17.0.3)
minitest (5.22.2)
mutex_m (0.2.0)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.7.3)
rainbow (3.1.1)
regexp_parser (2.5.0)
rexml (3.2.5)
rubocop (1.30.1)
regexp_parser (2.9.0)
rexml (3.2.6)
rubocop (1.62.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.1.0.0)
parser (>= 3.3.0.2)
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.31.1, < 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)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.1)
parser (>= 3.3.0.4)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
rubocop-discourse (3.7.1)
activesupport (>= 6.1)
rubocop (>= 1.59.0)
rubocop-capybara (>= 2.0.0)
rubocop-factory_bot (>= 2.0.0)
rubocop-rspec (>= 2.25.0)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-rspec (2.27.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
PLATFORMS
ruby
DEPENDENCIES
rubocop-discourse
syntax_tree
translations-manager!
BUNDLED WITH
2.2.19
2.5.4
+2 -1
View File
@@ -9,7 +9,8 @@ module DiscourseSubscriptions
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
@@ -5,6 +5,9 @@ module DiscourseSubscriptions
class CouponsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
def index
@@ -18,23 +21,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 +47,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
@@ -5,6 +5,8 @@ module DiscourseSubscriptions
class PlansController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
def index
@@ -27,15 +29,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 +54,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 +71,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
@@ -5,6 +5,8 @@ module DiscourseSubscriptions
class ProductsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
def index
@@ -27,20 +29,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 +48,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 +55,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 +70,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 +86,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
@@ -3,6 +3,8 @@
module DiscourseSubscriptions
module Admin
class SubscriptionsController < ::Admin::AdminController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
before_action :set_api_key
@@ -16,20 +18,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 +55,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 +71,6 @@ module DiscourseSubscriptions
end
render_json_dump subscription
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -74,7 +79,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 +94,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
@@ -2,6 +2,8 @@
module DiscourseSubscriptions
class AdminController < ::Admin::AdminController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
def index
head 200
end
@@ -4,7 +4,11 @@ module DiscourseSubscriptions
class HooksController < ::ApplicationController
include DiscourseSubscriptions::Group
include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
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 +16,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
@@ -4,31 +4,14 @@ module DiscourseSubscriptions
class SubscribeController < ::ApplicationController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
requires_login except: [:index, :contributors, :show]
requires_login except: %i[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
head 200
end
def contributors
@@ -36,7 +19,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 +36,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 +45,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 +112,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 +129,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 +149,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
@@ -4,6 +4,9 @@ module DiscourseSubscriptions
module User
class PaymentsController < ::ApplicationController
include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
requires_login
@@ -22,7 +25,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 +34,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 +42,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
@@ -5,6 +5,9 @@ module DiscourseSubscriptions
class SubscriptionsController < ::ApplicationController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key
requires_login
@@ -12,24 +15,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 +41,6 @@ module DiscourseSubscriptions
end
render_json_dump subscriptions
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -51,18 +50,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
@@ -51,6 +51,8 @@ module DiscourseSubscriptions
"KR"
when "sgd"
"S$"
when "zar"
"R"
else
"$"
end
@@ -23,9 +23,7 @@ module DiscourseSubscriptions
# 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
end
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
@@ -57,7 +55,7 @@ module DiscourseSubscriptions
protected
def goal_met_date_key
'subscriptions_goal_met_date'
"subscriptions_goal_met_date"
end
def check_goal_status
@@ -91,9 +89,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 +102,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 +121,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 +134,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 +146,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 +162,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 +183,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 +201,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}}
@@ -1,11 +1,11 @@
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { equal } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { setting } from "discourse/lib/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
@@ -58,12 +58,14 @@ export default Component.extend({
}
if (this.currentUser && this.showContributors) {
return ajax("/s/contributors", { method: "get" }).then((result) => {
this.setProperties({
contributors: result,
loading: false,
});
});
return ajax("/subscriptions/contributors", { method: "get" }).then(
(result) => {
this.setProperties({
contributors: result,
loading: false,
});
}
);
}
},
@@ -96,6 +98,7 @@ export default Component.extend({
},
willDestroyElement() {
this._super(...arguments);
document.body.classList.remove(SIDEBAR_BODY_CLASS);
},
@@ -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>

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