All pages
Powered by GitBook
1 of 21

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Development

Documentation for coders contributing to and using the Open Collective software application.

You can contact us on Discord engineering channel if you have any question.

Contribution Guide

We're happy to have you contributing to our codebase! We recommend you go through the following guide.

Technical Requirements

You'll need to have some basic programming experience with the technologies and tools we use.

Tools

  • Git & Github - Clone, commit and open a PR using Git with GitHub. Check out the following tutorials:

    • Introduction to git

    • Introduction to GitHub

    • Popular git commands and how to use them

    • Git commands in depth

    • Mastering Markdown

    • Markdown Tutorial

Languages & Frameworks

  • JavaScript/Node.js - We recommend having basic experience working with Node, which Open Collective is written in (frontend & backend). Check out these free JavaScript & Node tutorials:

    • Javascript

      • Introduction to basic principles of Javascript

      • Introduction to Javascript - w/ Advanced concepts

      • An interactive Javascript tutorial

    • Node.js

      • Quick introduction to Node.js

      • Introduction to Node.js - w/ quizzes

      • When, how and why to use Node.js

      • Differences between Javascript and Node.js

  • GraphQL - Our API uses GraphQL, powered by Sequelize and PostgreSQL. Understanding how this work is important to contributing to or fixing the majority of the issues on our API. To learn more, check out these tutorials & articles:

    • What is GraphQL and how to use it

    • Basic concept of GraphQL

    • Getting GraphQL running

    • Practical GraphQL tutorial

  • React & Next.js - You'll need to understand React and Next.js to contribute to issues on the frontend. Check out the following links:

    • React

      • Basic React Concepts

      • The Beginner react roadmap - path to mastering react

      • React Official documentation

    • Next.js

      • Basic introduction to Next.js

      • Next.js Official Documentation

      • Basic concepts in Next.js

      • Introduction to Next.js - w/ Advanced concepts

Project Structure

The project's core repositories are divided into three:

  • opencollective/opencollective - Here is where we manage issues and community discussions. Our issues are all labelled with a complexity label. We recommend starting with simple issues ( complexity -> simple).

  • opencollective/opencollective-frontend - This repository contains our frontend code. You can find more information in the setup section of this guide.

  • opencollective/opencollective-api - This contains our API code. If you enjoy working on the backend, you can set up our API locally. To learn about setting it up, check out the setup section below.

Project Setup

This section explains how you can get Open Collective running locally on your computer.

Frontend

Setting up the frontend is straightforward. We've provided a comprehensive guide in a separate document that explains how to set up the project.

Setup guide

https://github.com/opencollective/opencollective-frontend/blob/main/README.md

NOTE: If you're only contributing frontend code, you don't need to setup the API.

API

The API setup requires more effort than the frontend, as it requires installing the PostgreSQL. You might experience difficulty setting up the API on a Windows environment. We recommend using a Unix environment.

Just like the front end, we have a separate document for the setup.

Setup guide

https://github.com/opencollective/opencollective-api/blob/main/README.md

Others

Design Contribution

Like to contribute to our design? Checkout our design contribution guidelines.

Commit conventions

Before you make your first commit, read through our commit convention, provided in the link below:

https://github.com/opencollective/opencollective-frontend/blob/main/CONTRIBUTING.md

Bounty Program

We recommend you learn more about our bounty program through the link below:

https://docs.opencollective.com/help/developers/bounties

Ask for Help

If you are stuck or have a question, join our Discord #engineering channel through the link below:

discord.opencollective.com

We're trying our best to make our documentation better. We encourage you to give suggestions on how we can improve it.

GitHub Permissions

When connecting your Open Collective account to GitHub you will be asked to grant these permissions:

  • Code

  • Issues

  • Pull Requests

  • Wikis

  • Settings

  • Webhooks services

  • Deploy Keys

We recognize that this is asking for a lot of permissions. We looked into it thoroughly, and unfortunately we need all of these permissions for Open Collective functionality to work.

For more details, see the discussion on the associated issue.

Testing features

A document for QA testing and developers to know how to test features in a non-production environment

Manual Reporting

To manually run the monthly report:

1- Update the template: https://github.com/opencollective/opencollective-api/blob/main/templates/emails/collective.monthlyreport.hbs

2- If you want to preview some of the emails, you can run it locally with

PG_DATABASE=opencollective_prod_snapshot DEBUG=preview npm run cron:monthly

(it won't send real emails but it will output links to preview them in the browser)

3- Run bash on heroku production:

heroku run bash --app opencollective-prod-api

4- Run the monthly cron job manually:

npm run cron:monthly

If you want to only run it for certain collectives, modify the query here: https://github.com/opencollective/opencollective-api/blob/main/cron/monthly/collective-report.js

Conversations

A document for QA testing and developers about testing conversations a non-production environment

Prerequisite : Enable conversations

  • For newly created collectives, conversations are enabled by default!

  • Otherwise go to the settings page (/{collective}/admin), click on Conversations and enable

Create conversations

Go to /{collective}/conversations/new

View conversations list

Go to /{collective}/conversations

Members

Get list of members

We have a CSV or JSON data if you prefer.

GET https://opencollective.com/:collective/members/[all|users|organizations].json

E.g.

[
  {
    "MemberId": 8764,
"createdAt": "2017-11-03 09:36",
"type": "ORGANIZATION",
"role": "BACKER",
"tier": "Backer",
"isActive": true,
"totalAmountDonated": 2,
"currency": "USD",
"lastTransactionAt": "2017-11-03 09:36",
"lastTransactionAmount": 2,
"profile": "https://opencollective.com/freegames661",
"name": "Freegames66",
"description": null,
"image": null,
"email": null,
"twitterHandle": "freegames66",
"website": "http://www.freegames66.com"
},
  ...
]
UI for exporting
https://opencollective.com/webpack/members/organizations.json

Best Practice Guidelines

Describes what we expect from new code. Also includes special tips to help you getting started!

General rules

  • When adding new dependencies, we use fixed versions.

  • Don't commit package-lock.json if you're not making any changes to the libraries.

  • If the issue you're working on requires changes in both API and Frontend, give your Git branches the same name. CI will automatically pull the correct API's branch when testing the Frontend.

  • We love screenshots - or better: screencasts. Include some in your pull requests to demonstrate your changes and you will have our eternal gratitude.

Frontend rules

  • I18n

    • The strings must be internationalized. See /help/developers/translations.

    • Update the language files npm run build:langs and commit them to reflect the changes.

  • Libraries

    • Whenever it's possible, we must use styled-components to write styles. See OC Styleguide.

    • We're getting rid of material-ui. We still rely on this library for some stuff but please don't use it directly.

    • Icons must be imported from the styled-icons library.

  • Testing

    • Tests written with Cypress must follow our good practices conventions.

Collective's locations

How we deal with collective's addresses and countries

GraphQL API

On the GraphQL side things are easy, you can just fetch collective.location and you'll get an object like:

{
    name: { type: GraphQLString },
    address: { type: GraphQLString },
    country: { type: GraphQLString },
    lat: { type: GraphQLFloat },
    long: { type: GraphQLFloat },
}

Database

On the database side things are organized this way:

  • geoLocationLatLong => coordinates (eg. POINT (43.6515899 -70.29052239999999))

  • countryISO => two letters country code (eg. FR, BE...etc)

  • address => postal address normally without country (eg. 12 opensource avenue, 7500 Paris)

  • locationName => a name for the location (eg. Google Headquarters)

README Integration

To get help generating an integration like or , follow these steps:

  1. Install to make it easy to compare PRs in the last step

  2. Install opencollective-setup

  3. Get a personal token from . Check all the repo related permissions.

  4. Create a file in your home directory (on Mac OS X or Linux) called .opencollective.json and add token in it:

  5. Run cli for a given repo:

    Ex: To integrate with MochaJS (), run:

    -i makes it interactive.

  6. Answer questions asked by the script - usually defaults are good to go with. Verify that the slug of the project is the same as the one in the database (script guesses at it and is usually right).

  7. Script attempts to do several integrations across README.md, CONTRIBUTORS.md and ISSUE_TEMPLATE.md. The most important ones are the two integrations on README.md: backers and sponsor badges at the top and adding backer/sponsor section near the bottom.

PayPal

Technical details about how we implement PayPal and how to get started developing with it.

Developing / testing sandbox

Buyer account

This is the account you'll use to make the (fake) payment. Go to , login with your personnal PayPal account then create a test account.

Merchant account

  1. Create an app here:

  2. Use the generated merchant credentials to set the following variables in API's .env:

3. Encrypt your client secret, from the API repository:

4. Manually create a ConnectedAccount with your clientId and your encrypted clientSecret:

5. Create buyer's credentials on

And you're ready to go. Use the credentials generated in step 2. to authenticate when ordering.

Known issues

  • The button may require multiple clicks to trigger on dev or staging. It should not affect production (see )

TransferWise

Technical details about how we implement TransferWise and how to get started developing with it.

TransferWise integration can be used to automate expense payment as a way to provide one-click wire transfer for expenses.

Strong Customer Authentication

In order to create a new TRANSFERWISE_PRIVATE_KEY you'll need to generate a new key pair:

After that, you can encode the private key using base64 and save it as an environment variable:

Developer Sandbox

API Token

  1. .

    • Two-factor authentication (2FA) code for sandbox login is 111111.

  2. .

  3. Add a new Token.

  4. Done, now you can copy your token!

Connect Account to your Host

  1. Manually create a ConnectedAccount with generated clientId and clientSecret:

    • We're currently defaulting to your Business profile if two profiles exists. If you have two profiles and want to use your personal one, make sure to add { "type": "personal" } to the data column of the created Conencted Account.

API

GraphQL API

The future of the Open Collective API is our public GraphQL API. You can check the documentation on .

Learn more about it here:

REST API

Our REST API is still supported but we're not working on it anymore.

    • Get list of members

    • Get collective info

    • Get members

    • Get members per tier

    • Get transactions from collective

PDF Service

The PDF service supports passing either a Personal-Token (for personal tokens) or an Authorization header (for OAuth).

Getting the receipt for a contribution

Getting the PDF for an Invoice Expense

Getting a bundled receipt for a fiscal host/period

PAYPAL_ENVIRONMENT=sandbox
PAYPAL_APP_ID=APP-________
npm run script scripts/encrypt.js PAYPAL_CLIENT_SECRET
INSERT INTO "ConnectedAccounts" ("service", "clientId", "token", "CollectiveId")
VALUES (E'paypal', clientId, clientSecret, hostCollectiveId);
https://developer.paypal.com/developer/accounts/create
https://developer.paypal.com/developer/applications/create
https://developer.paypal.com/developer/accounts/create
https://github.com/paypal/paypal-checkout/issues/471
openssl genrsa -out private.pem 2048
openssl rsa -pubout -in private.pem -out public.pem
TRANSFERWISE_PRIVATE_KEY=$(cat private.pem | base64 -w 0)
INSERT INTO "ConnectedAccounts" ("service", "token", "CollectiveId")
VALUES (E'transferwise', token, hostCollectiveId);
Sign up for a developer account sandbox
Go to settings
GET https://pdf.opencollective.com/receipts/transactions/:transactionUUID/receipt.pdf
GET https://pdf.opencollective.com/expense/:expenseUUID/invoice.pdf
GET https://pdf.opencollective.com/receipts/collectives/:fromCollective/:host/:fromDate/:toDate/receipt.pdf
https://graphql-docs-v2.opencollective.com
https://medium.com/open-collective/open-collective-graphql-api-preview-3b42ed1d55ff
Members
Collectives
$ npm install -g opencollective-setup
{ "github_token": "[YOUR_TOKEN]" }
$ opencollective-setup setup -r [repo_owner/repo_name] -i
opencollective-setup -r mochajs/mocha -i
this
this
Macdown
GitHub's token page
https://github.com/mochajs/mocha

Virtual Cards

How to setup Virtual Cards in local development

This documentation is about development. To setup a production environment, see https://docs.opencollective.com/help/fiscal-hosts/virtual-cards

Setup Stripe in development

On staging, https://staging.opencollective.com/opensource/ is already configured to issue virtual cards. If what you're working on doesn't require any API changes, feeel free to use it directly.

The steps to configure Stripe issuing on your local setup are:

  1. Ask Stripe to enable Issuing on your account https://dashboard.stripe.com/setup/issuing/activate (if you're managing multiple accounts, make sure to pick the right account on top)

  2. Top Up the Issuing balance with a reasonable amount ($1000 ?, $10,000 ?)

  3. Create a generic Card Holder and make sure it's the only one (if you have more, let us know)

  4. Configure webhook and enable for all issuing_authorization.* and issuing_transaction.* (5 events) https://api.opencollective.com/webhook/stripe

  5. Configure default authorization process and webhook (deny or allow) (Optional) https://dashboard.stripe.com/settings/issuing/authorizations

  6. Create new dedicated Restricted Secret Key, select write for all Issuing features (Name: Restricted Issuing)

  7. Contact Open Collective Engineering team to configure Restricted Secret Key production (update-connected-account-stripe-token)

  8. Contact Open Collective Engineering team to configure Webhook Signing secret to connected account (stripeEndpointSecret)

  9. Ask Open Collective Engineering team to enable feature in Collective settings (privacyVcc)

Setup privacy in development

Privacy is deprecated and shouldn't be used for new developments

  1. Create an account on https://privacy.com

  2. Go to https://privacy.com/account, scroll down to "Enable API", toggle the switch, click on Sandbox and copy the Sandbox API key (NOT THE PRODUCTION ONE)

3. Connect to the database with your favorite tool (psql, DBeaver, Postico, etc.) and search for the host you want to enable Privacy for. Edit its settings to set features.privacyVcc to true

4. Open the host settings and go to the "Fiscal host settings" > "Sending Money" section

5. Paste your API Key in the "API Key" field and click on "Connect Privacy".

Post-Donation Redirect

How to redirect people to your website after they make a donation

You can create a custom URL to donate a specific amount (and frequency) by appending /donate, /pay or /contribute to your collective url: e.g.

You can also append to that url a redirect parameter. That way, after the user donates money, they will be redirected to your URL and we will pass the transactionid.

E.g.

When you donate you will be redirected to:

Then you can call our API to get all the details about that transaction:

You can get your API key in your "Applications" page that you can access from your logged in user dropdown menu.

Example of the data being returned:

https://opencollective.com/webpack/donate/100/month/bronze%20sponsorship
https://opencollective.com/webpack/pay/1000/invoice%201234
https://opencollective.com/octobox/pay/100/month/support%20the%20community?redirect=https://octobox.io/callback
 https://octobox.io/callback?transactionid=117fe88f-3fc7-4634-b78c-05b0fd0cf7d8
https://api.opencollective.com/v1/collectives/octobox/transactions/117fe88f-3fc7-4634-b78c-05b0fd0cf7d8?apiKey=xxxxx
{
    "result": {
        "id": 134368,
        "uuid": null,
        "type": "CREDIT",
        "createdAt": "Wed Nov 07 2018 09:05:06 GMT+0000 (UTC)",
        "description": "Monthly donation to Octobox",
        "amount": 10000,
        "currency": "USD",
        "hostCurrency": "USD",
        "hostCurrencyFxRate": 1,
        "netAmountInCollectiveCurrency": 8680,
        "hostFeeInHostCurrency": -500,
        "platformFeeInHostCurrency": -500,
        "paymentProcessorFeeInHostCurrency": -320,
        "paymentMethod": {
            "id": 34801,
            "service": "stripe",
            "name": "4242"
        },
        "fromCollective": {
            "id": 23657,
            "slug": "anonymous1338",
            "name": "anonymous",
            "image": null
        },
        "collective": {
            "id": 413,
            "slug": "octobox",
            "name": "Octobox",
            "image": "https://opencollective-production.s3-us-west-1.amazonaws.com/4e491a10-aae0-11e8-a91b-df5253215e9d.png"
        },
        "host": {
            "id": 11004,
            "slug": "opensourcecollective",
            "name": "Open Source Collective 501c6 (Non Profit)",
            "image": "https://opencollective-production.s3-us-west-1.amazonaws.com/5f4a3920-11b6-11e8-b28d-b359f3c5ca14.png"
        },
        "order": {
            "id": 33193,
            "status": "ACTIVE",
            "subscription": {
                "id": 26426,
                "interval": "month"
            }
        }
    }
}

Events

List events

/:collectiveSlug/events.json

E.g. https://opencollective.com/sustainoss/events.json?limit=10&offset=0

[
  {
    "id": 8770,
    "name": "Sustain Summit 2017 - San Francisco",
    "description": "A one day conversation for Open Source Software sustainers",
    "slug": "2017-442ev",
    "image": "https://opencollective-production.s3.us-west-1.amazonaws.com/6efa1900-d715-11e9-83ac-07a1cc2eb17c.png",
    "startsAt": "Mon Jun 19 2017 17:00:00 GMT+0000 (UTC)",
    "endsAt": "Thu Mar 16 2017 01:00:00 GMT+0000 (UTC)",
    "location": {
      "name": "GitHub HQ",
      "address": "88 Colin P Kelly Jr Street, San Francisco, CA",
      "lat": 37.782267,
      "long": -122.391248
    },
    "url": "https://opencollective.com/sustainoss/events/2017-442ev",
    "info": "https://opencollective.com/sustainoss/events/2017-442ev.json"
  }
]

Parameters:

  • limit: number of events to return

  • offset: number of events to skip (for pagination)

Notes:

  • url is the url of the page of the event on opencollective.com

  • info is the url to get the detailed information about the event in json

Get event info

/:collectiveSlug/events/:eventSlug.json

E.g. https://opencollective.com/sustainoss/events/2017-442ev.json

{
  "id": 8770,
  "name": "Sustain Summit 2017 - San Francisco",
  "description": "A one day conversation for Open Source Software sustainers",
  "longDescription": "A one day conversation for Open Source Software sustainers\n\nNo keynotes, expo halls or talks.\nOnly discussions about how to get more resources to support digital infrastructure.\n\n# What\nA guided discussion about getting and distributing money or services to the Open Source community. The conversation will be facilitated by [Gunner](https://aspirationtech.org/about/people/gunner) from AspirationTech.\n\n# Sustainer?\nA sustainer is someone who evangelizes and passionately advocates for the needs of open source contributors.\n\nThey educate the public through blog posts, talks & social media about the digital infrastructure that they use everyday and for the most part, take for granted.\n\nThey convince the companies that they work for to donate money, infrastructure, goods and/or services to the community at large. They also talk to the companies that they don’t work for about the benefits sustaining open source for the future.\n\n# Connect\n- Slack\nhttps://changelog.com/community\n\\#sustain\n- Twitter\n[@sustainoss](https://twitter.com/sustainoss)\n- GitHub\nhttps://github.com/sustainers/\n\n# Scholarships\nWe welcome everyone who wants to contribute to this conversation. Email us [email protected] if the ticket doesn't fit your budget.\n\n# SUSTAIN IS SOLD OUT 🎉🎉 \nWe are still accepting sponsorships if you'd like to contribute. ",
  "slug": "2017-442ev",
  "image": "https://opencollective-production.s3.us-west-1.amazonaws.com/6efa1900-d715-11e9-83ac-07a1cc2eb17c.png",
  "startsAt": "Mon Jun 19 2017 17:00:00 GMT+0000 (UTC)",
  "endsAt": "Thu Mar 16 2017 01:00:00 GMT+0000 (UTC)",
  "location": {
    "name": "GitHub HQ",
    "address": "88 Colin P Kelly Jr Street, San Francisco, CA",
    "lat": 37.782267,
    "long": -122.391248
  },
  "currency": "USD",
  "tiers": [
    {
      "id": 10,
      "name": "sponsor",
      "description": "Contribute to the travel & accommodation fund your logo/link on website\n$25 credit for sticker swap table.",
      "amount": 100000
    }
  ],
  "url": "https://opencollective.com/sustainoss/events/2017-442ev",
  "attendees": "https://opencollective.com/sustainoss/events/2017-442ev/attendees.json"
}

Notes:

  • url is the url of the page of the event on opencollective.com

  • attendees is the url to get the list of attendees in JSON

Bounties

Open Collective bounty program

Get paid to contribute to Open Source!

The Open Collective engineering team is small, and we're always looking for new contributors to our Open Source codebases. Our Bounty program is an opportunity to solve issues that could be neglected otherwise. Contributors who fix these issues will be rewarded financially.

Principles

With money, but not only about money

Our bounty program is about creating opportunities for our community to contribute to Open Collective, to make it their own. It also gives us an opportunity to get to know developers who we could potentially work with more in the future. We celebrate making open source contributions more sustainable by paying, but money alone shouldn't be the primary motivation for participation.

No compromise on quality

We're not able to accept pull requests that aren't completed to a high standard in a reasonable timeframe. Please only pick up bounties that you are confident you can complete at your current knowledge and skill level. We will not accept pull requests or pay bounties for code that's not up to the standard we need to maintain for the Collectives who rely on this platform.

Our dev team is happy to answer questions and provide some limited support, but we don't have capacity to mentor junior developers through the bounty program.

For general guidelines about what's expected in the code, see more info here.

How it works

For reference, until July 2020, we used the following model:

  • $100: minimal or unknown complexity

  • $200: simple complexity

  • $500: medium complexity

Note: we are not able to pay bounties to people based in countries sanctioned by the United States, or countries where US sanctions are so widespread that our payment processors no longer serve them.

But since then we have moved to a more flexible one. Based on the importance of the issue and its complexity, we attach a bounty between $100 and $1000 to the ticket.

We want to attract quality contributions. The issue will only be considered complete and approved for payment if the Pull Request is merged by an Open Collective Core Developer.

Workflow for Bounty Program Contributors

  1. Search for issues with attached bounties:

    • Bounties for all repositories

  2. Express interest by commenting on the issue and ask to be assigned

  3. Open a Pull Request and ask for feedback and review

  4. Incorporate feedback from Core Developers, if applicable

  5. PR is reviewed, approved, and merged by a Core Developer

  6. Get paid:

    • If you can issue an invoice, submit it as expense to Ofitech.

    • If you can't issue an invoice, ask to be rewarded with an Open Collective Gift Card

See more info about getting paid through Open Collective

Issues tagged as "bounty candidate" are issues that we are considering to add bounties for, but that are not bounties yet - either because they lack proper specifications, a team consensus, or because we don't have the bandwidth to review it at the moment. Feel free to comment on such issues to ask for a bounty to be added if it's something you're willing to work on.

Financial compensation can only happen if the issue has a "bounty" tag with a pre-defined amount. In other words, completing a bounty candidate that didn't receive the "bounty" tag will not make you eligible for the bounty program.

Supported payment options

The options supported for paying bounties are the ones supported by Ofitech, namely:

  • PayPal

  • Bank account transfers (to countries not in the US sanctioned list)

Workflow for Core Contributors

  1. Make sure the issue is understandable for newcomers and expectations are clearly set

  2. Tag issues with the "bounty" label and amount (e.g. "$100")

  3. Add a comment with a basic explanation of the Bounty process and link to BOUNTY.md

    A $100 bounty was attached to this issue. Anyone submitting a Pull Request will be rewarded with $100 when the Pull Request is reviewed, accepted and merged. More info.

  4. Make sure the issue is understandable for newcomers and expectations are clearly set

Contributor Ladder

  1. First Time Contributors

    • Not part of the Open Collective GitHub organization

    • Fork our projects on GitHub and push changes on their forks

    • Have access to minimal or simple complexity issues

    • Should comment on bounty issues to get assigned (limited to one at a time)

  2. Contributors (at least 1 completed issue)

    • Added to the "Contributors" group in the Open Collective GitHub organization

    • Can push branches to the Open Collective repositories

    • Have access to minimal, simple or medium complexity issues

    • Can assign themselves bounty issues (limited to one at a time)

  3. Recurring Contributors (3 or more completed issues)

    • Added to the "Recurring Contributors" group in the Open Collective GitHub organization

    • Can assign themselves two bounty issues at a time

  4. Confirmed Contributors (3 or more completed issues including at least 2 with medium complexity)

    • Added to the "Confirmed Contributors" group on the Open Collective GitHub organization

    • Become candidates to work on complex issues or projects on a negotiable per-project or hourly rate

Architecture

Collectives

In Open Collective, every entity is a collective and can be accessed publicly via their unique slug https://opencollective.com/:slug. In our public API, Collectives are usually refered to as Accounts. You can think about it like "profiles" that's what we really store in that table.

A Collective can be of type:

  • COLLECTIVE e.g. Webpack

  • EVENT e.g. BrusselsTogether Meetup 4

  • ORGANIZATION e.g. iDoneThis, DigitalOcean, etc.

  • USER e.g. xdamman

  • PROJECT

  • FUND

Members

A Member connects two profiles together. It can have multiple roles (one role per row):

  • HOST legal holder of the bank account that holds the money on behalf of the collective

  • ADMIN users who can approve expenses for the collective

  • MEMBER aka core contributors

  • BACKER users who gave money to the collective

Orders

An Order is the intent to give money to an Account. It is created by a UserId on behalf of a collective (which can be their own UserCollective or any other Collective that they are a member of).

Attributes:

Attribute

Definition

Example

UserCollectiveId

User who created the order

/xdamman

FromCollectiveId

Source of the money

/digitalocean

CollectiveId

Destination of the money

/webpack

currency

currency of the ToCollectiveId

USD

amount

amount in cents

10000

SubscriptionId

References recurring subscription

status

status of the order

PENDING -> APPROVED|REJECTED -> PAID

Transactions

A Transaction records money moving from one account to another ac. In this example, a collective webpack is giving €100 to Women Who Code Berlin hosted by Women Who Code 501(c)(3).

Attribute

Definition

Example

OrderId

References the order

1

FromCollectiveId

Source of the money (virtual account)

/webpack

ToCollectiveId

Destination of the money (virtual account)

/wwcodeberlin

PaymentMethodId

Payment method (e.g. if there wasn't enough funds in the FromCollectiveId)

NULL

FromHostId

Source of the money

/opensource

ToHostId

Destination of the money

/wwcode

FromHostAmount

total amount in cents paid by the host of the FromCollectiveId in the currency of the host

-11481 (-$114.81)

FromCollectiveAmount

total amount that increases/decreases the balance of the FromCollectiveId in the currency of the FromCollectiveId

-11481

paymentProcessorFeesInHostCurrency

fees for the payment processor in cents

hostFeesInHostCurrency

fees for the host in cents in the currency of the host (which might be different than the currency of the collective, e.g. WWCode (USD) and WWCode Berlin (EUR)

574 (5% of €100 in USD)

platformFeesInHostCurrency

fees for the platform (Open Collective)

574 (5% of €100 in USD)

ToHostAmount

net amount in cents received by the host of the ToCollectiveId in the currency of the ToHostId

9630 (€100 - (2.9% + $0.30) - €5 platform fee)

ToCollectiveAmount

total amount that increases/decreases the balance of the ToCollectiveId in the currency of the order

9580 (€96.30 - €5 host fee)

FromHostCurrency

currency of the FromHostId

USD

FromCollectiveCurrency

currency of the FromCollectiveId

USD

ToCollectiveCurrency

currency of the collective that receives the money

EUR

ToHostCurrency

currency of the order (currency of the ToCollectiveId)

USD

fxrate

Foreign eXchange Rate from the currency of the order (ToCollectiveCurrency) to the currency of the host of the FromCollectiveId (float)

1.15

Note: The Collective currency might be different than the Host Currency (both for the source "From" and the recipient "To"). The fxrate only takes into account the conversion between ToCollectiveCurrency to ToHostId.

Testing with Cypress

We use Cypress for our end-to-end tests. This page references our custom commands and the best practices that we try to follow.

Best practices

We should try to stick with https://docs.cypress.io/guides/references/best-practices.html as much as possible. In short, we should currently enforce:

Use data-cy to target DOM elements

Don’t target elements based on CSS attributes such as: id, class, tag Add data-* attributes to make it easy to target elements

Tests independence

Tests should not rely on each others results and should be repeatable (we must be able to run it multiple times consecutively). You can use commands like cy.signup() to ensure that you start from a fresh context.

Custom commands

To improve the testing experience and the readability of our tests, we have defined a set of custom commands in cypress/support/commands.js.

Login, signup and seeding

Both the cy.login and cy.signup commands accept a redirect parameter. You can use it to be redirected to a specific page as soon as the command succeed.

cy.login({email, redirect} = params)

Login with an existing account. If not provided in params, the email used for authentication will be [email protected].

Note that addresses formatted like test*@opencollective.com are a special case that let you login directly without the need to confirm your email. You can use the randomEmail helper to generate these.

cy.signup({user, redirect} = params)

Create an account and login with it. If no params is provided, the account will be created with a random email.

cy.createCollective({ type = 'ORGANIZATION', email =defaultTestUserEmail})

Helper to quickly create a collective that use designated by email will be an admin of.

cy.addCreditCardToCollective({ collectiveSlug })

Adds a default test credit card to the collective referenced by collectiveSlug

Forms

cy.fillStripeInput(container, cardParams)

Fills a stripe input.

  • container (optional) the DOM that contains the input

  • cardParams (optional) the credit card info. Defaults to a valid card. Keys:

    • creditCardNumber

    • expirationDate

    • cvcCode

    • postalCode

Emails

cy.openEmail(filterFunc)

This function is used to open an email in Cypress. If the command succeed, a page with the email is loaded and you'll be able to run all the usual cypress commands (cy.get, cy.contains...) to test it.

  • fiterFunc is a function used to filter the list of emails. As soon as it returns true, command will start to open the email. For a complete list of the fields you can use to filter the emails, see https://github.com/djfarrelly/MailDev/blob/master/docs/rest.md

Examples

// Will open the first email with where subject contains "Hello World"
cy.openEmail(({ subject }) => subject.includes('Hello World'));

// Will open the first email sent to `[email protected]`
cy.openEmail(({ to }) => to[0].address === '[email protected]' );

cy.getInbox()

Return the full inbox as a list of email objects. cy.openEmail should be privileged, but this one can be useful if you need to do more advanced verification like counting the number of emails or who the email was sent to.

> cy.getInbox()
[{
  "id":"XwgKAxto",
  "time":"2014-10-05T19:02:09.156Z",
  "from":[{
    "address":"[email protected]",
    "name":"Angelo Pappas"
  }],
  "to":[{
    "address":"[email protected]",
    "name":"Johnny Utah"
  }],
  "subject":"The ex-presidents are surfers",
  "text":"The wax at the bank was surfer wax!!!",
  "html":"<!DOCTYPE html><html><head></head><body><p>The wax at the bank was surfer wax!!!</p></body></html>",
  "headers":{
    "content-type":"multipart/mixed; boundary=\"---sinikael-?=_1-14125357291310.1947895612102002\"",
    "from":"Angelo Pappas <[email protected]>",
    "to":"Johnny Utah <[email protected]>",
    "subject":"The ex-presidents are surfers",
    "x-some-header":"1000",
    "x-mailer":"nodemailer (1.3.0; +http://www.nodemailer.com; SMTP/0.1.13[client:1.0.0])",
    "date":"Sun, 05 Oct 2014 19:02:09 +0000",
    "message-id":"<[email protected]>",
    "mime-version":"1.0"
  },
  "messageId":"[email protected]",
  "priority":"normal",
  "attachments":[{
    "contentType":"text/plain",
    "contentDisposition":"attachment",
    "fileName":"attachment-1.txt",
    "generatedFileName":"attachment-1.txt",
    "contentId":"0958713110a99ea2afc3b117c9d5feb3@mailparser",
    "stream":{
      "domain":null,
      "_events":{},
      "_maxListeners":10,
      "writable":true,
      "checksum":{"_binding":{}},
      "length":0,
      "charset":"UTF-8",
      "current":""
    },
    "checksum":"d41d8cd98f00b204e9800998ecf8427e"
  }],
  "envelope":{
    "from":"[email protected]",
    "to":["[email protected]"],
    "host":"djf-3.local",
    "remoteAddress":"127.0.0.1"
  }
}]

cy.clearInbox()

Clears the inbox. It is a good practice to run it in before to ensure that your test cannot be influenced by the emails sent in previous tests.

Collectives

Get collective info

Get detailed information about a collective:

/:collectiveSlug.:format(json|csv)

E.g.: https://opencollective.com/webpack.json

{
  "slug": "webpack",
  "currency": "USD",
  "image": "https://cl.ly/221T14472V23/icon-big_x6ot1e.png",
  "balance": 7614777,
  "yearlyIncome": 28499262,
  "backersCount": 556,
  "contributorsCount": 1098
}

Notes:

  • image is the logo of the collective

  • all amounts are in the smaller unit of the currency (cents)

  • backersCount includes both individual backers and organizations (sponsors)

  • yearlyIncome is the projection of the annual budget based on previous donations and monthly pledges

Get members

Returns all members of the collectives (core contributors, contributors, backers, sponsors)

/:collectiveSlug/members.:format(json|csv)

You can also filter by member type (USER or ORGANIZATION):

/:collectiveSlug/members/:memberType(all|users|organizations).:format(json|csv)

E.g.

  • https://opencollective.com/webpack/members.json?limit=10&offset=0

  • https://opencollective.com/webpack/members/all.json?limit=10&offset=0

[
  {
    "MemberId": 8198,
    "createdAt": "2017-10-25 09:52",
    "type": "USER",
    "role": "BACKER",
    "tier": "Backer",
    "isActive": true,
    "totalAmountDonated": 1000,
    "currency": "USD",
    "lastTransactionAt": "2018-02-01 10:53",
    "lastTransactionAmount": 200,
    "profile": "https://opencollective.com/ralph03",
    "name": "Ralph03",
    "company": null,
    "description": "",
    "image": "https://opencollective-production.s3-us-west-1.amazonaws.com/882e5a00-ce64-11e7-ae39-cb1f4eb45be3.jpg",
    "email": null,
    "twitter": null,
    "github": "https://github.com/kazup01",
    "website": null
  },
  ...
]

Parameters:

  • limit: number of members to return per call

  • offset: number of members to skip (for pagination)

Notes:

  • github is verified via oauth but twitter is not

  • email returns null unless you make an authenticated call using the accessToken of one of the admins of the collective

  • all amounts are in the smaller unit of the currency (cents)

  • type can be USER, ORGANIZATION or COLLECTIVE

  • role can be ADMIN, MEMBER, BACKER, ATTENDEE, FOLLOWER

  • tier is the name of the tier

  • isActive specifies if the backer has an active subscription

Get members per tier

/:collectiveSlug/[all|users|organizations].:format(json|csv)?TierId=:TierId

You can find the TierId by looking at the URL after clicking on a Tier Card on the collective page (e.g. TierId for https://opencollective.com/webpack/order/266 is 266).

Alternatively, you can also use the slug of a tier:

/:collectiveSlug/tiers/:tierSlug/[all|users|organizations].format(json|csv)

E.g.

  • https://opencollective.com/babel/members/all.json?TierId=1906&limit=10&offset=0

  • https://opencollective.com/babel/tiers/gold-sponsors/all.json?limit=10&offset=0

[
  {
    "MemberId": 5485,
    "createdAt": "2017-07-07 16:44",
    "type": "ORGANIZATION",
    "role": "BACKER",
    "tier": "Gold Sponsors",
    "isActive": true,
    "totalAmountDonated": 2600,
    "currency": "USD",
    "lastTransactionAt": "2018-02-01 20:23",
    "lastTransactionAmount": 1000,
    "profile": "https://opencollective.com/amp",
    "name": "AMP Project",
    "company": "",
    "description": null,
    "image": "https://opencollective-production.s3-us-west-1.amazonaws.com/68ed8b70-ebf2-11e6-9958-cb7e79408c56.png",
    "email": null,
    "twitter": "https://twitter.com/amphtml",
    "github": null,
    "website": "https://www.ampproject.org/"
  },
  {
    "MemberId": 8263,
    "createdAt": "2017-10-26 23:08",
    "type": "ORGANIZATION",
    "role": "BACKER",
    "tier": "Gold Sponsors",
    "isActive": true,
    "totalAmountDonated": 5000,
    "currency": "USD",
    "lastTransactionAt": "2018-02-02 00:08",
    "lastTransactionAmount": 1000,
    "profile": "https://opencollective.com/fbopensource",
    "name": "Facebook Open Source",
    "company": null,
    "description": "Facebook Open Source Team",
    "image": "http://res.cloudinary.com/opencollective/image/upload/v1508519428/S9gk78AS_400x400_fulq2l.jpg",
    "email": null,
    "twitter": "https://twitter.com/fbOpenSource",
    "github": null,
    "website": "https://code.facebook.com/projects/"
  }
]

Get transactions from collective

/v1/collectives/:collectiveSlug/transactions?type=:type&limit=:limit&offset=:offset&dateFrom=:dateFrom&dateTo=:dateTo&type=:includeVirtualCards

Return All Transactions of a collective given its slug.

Parameters

  • limit: number of members to return per call

  • offset: number of members to skip (for pagination)

  • type: filter transactions of type DEBIT or CREDIT

  • dateFrom: the start date (format YYYY-MM-DD) to be considered when returning the data

  • dateTo: the end date (format YYYY-MM-DD) to be considered when returning the data

  • includeVirtualCards: a boolean that, if true, will include the transactions generated by all virtual cards issued by the specified collective

Curl command

curl "https://api.opencollective.com/v1/collectives/opencollective-company/transactions" \
  -H "Content-Type: application/json"\
  -H "Client-Id: ${ClientId}"

PS: For more details on how to have a Client ID/API Key, get in touch.

E.g.

  • Including Virtual Card transactions (transactions that used a virtual card that was issued by the collective): https://api.opencollective.com/v1/collectives/opencollectiveinc/transactions?api_key=YOUR_API_KEY&includeVirtualCards=true

  • NOT Including Virtual Cards: https://api.opencollective.com/v1/collectives/opencollectiveinc/transactions?api_key=YOUR_API_KEY

  • Using limit=20, type=DEBIT and offset=5: https://api.opencollective.com/v1/collectives/opencollectiveinc/transactions?api_key=YOUR_API_KEY&includeVirtualCards=true&limit=20&type=DEBIT&offset=5

Output

The output will be a json with a result property that will contain an array. here is an example:

{
   "result": [
      {
         "id": 9047,
         "uuid": null,
         "type": "CREDIT",
         "amount": 500,
         "currency": "USD",
         "hostCurrency": "USD",
         "hostCurrencyFxRate": 1,
         "hostFeeInHostCurrency": -25,
         "platformFeeInHostCurrency": -25,
         "paymentProcessorFeeInHostCurrency": -45,
         "netAmountInCollectiveCurrency": 405,
         "createdAt": "Sun Apr 30 2017 22:33:49 GMT-0400 (Eastern Daylight Time)",
         "updatedAt": "Thu Mar 08 2018 15:24:33 GMT-0500 (Eastern Standard Time)",
         "host": {
            "id": 8686,
            "slug": "opencollectiveinc"
         },
         "createdByUser": {
            "id": 3605,
            "email": null
         },
         "fromCollective": {
            "id": 4505,
            "slug": "christinabowen"
         },
         "collective": {
            "id": 1,
            "slug": "opencollective-company"
         },
         "paymentMethod": {
            "id": 2198
         }
      },
      {
         "id": 7698,
         "uuid": null,
         "type": "CREDIT",
         "amount": 500,
         "currency": "USD",
         "hostCurrency": "USD",
         "hostCurrencyFxRate": 1,
         "hostFeeInHostCurrency": -25,
         "platformFeeInHostCurrency": -25,
         "paymentProcessorFeeInHostCurrency": -45,
         "netAmountInCollectiveCurrency": 405,
         "createdAt": "Fri Mar 31 2017 22:25:57 GMT-0400 (Eastern Daylight Time)",
         "updatedAt": "Thu Mar 08 2018 15:23:18 GMT-0500 (Eastern Standard Time)",
         "host": {
            "id": 8686,
            "slug": "opencollectiveinc"
         },
         "createdByUser": {
            "id": 3605,
            "email": null
         },
         "fromCollective": {
            "id": 4505,
            "slug": "christinabowen"
         },
         "collective": {
            "id": 1,
            "slug": "opencollective-company"
         },
         "paymentMethod": {
            "id": 2198
         }
      }
   ]
}

Internationalization (i18n) system

Documenting how we handle translations in the code

We use react-intl to manage our translations. They are extracted from the code to src/lang/${locale}.json files using the npm run build:langs command (CI will notify you if the translation files are outdated). Don't translate the strings directly in the files, we use Crowdin to manage our translatations.

Good practices

Use "select" when a value has a limited number of options

Example

<FormattedMessage
    defaultMessage="{action, select, delete {Delete this} archive {Archive this} other {Do something with this}}"
    values={{ action: 'delete' }}
/>

// => "Delete this"

<FormattedMessage
    defaultMessage="{action, select, delete {Delete this} archive {Archive this} other {Do something with this}}"
    values={{ action: 'eat' }}
/>

// => "Do something with this"
  • defaultMessage string breakdown:

    • action variable name

    • select keyword

    • delete and archive possible values

    • other all other values will use this key

An exception to this rule: very common enums or the ones with many possible values should be implemented as a separate file listing all values because:

  • Re-usability

  • A map of translations is easier to read than a long select string with tons of options

See i18n-member-role as an example.

Don't assume word's order stays the same in other languages

The order of the words may change from a language to another. For this reason we must always pass the values to be replaced in values so their order can later be changed.

Example

// Bad
<div>
    <FormattedMessage id="str" defaultMessage="Pending approval from " />
    <Link route={`/${host.slug}`}>{host.name} </Link>
</div>

// Good
<div>
    <FormattedMessage  
        defaultMessage="Pending approval from {host}" 
        values ={{ 
          host: <Link route={`/${host.slug}`}>{host.name} </Link>
        }}
    />
</div>

Don't split strings

Splitting a string is alway problematic, because translators loose the context: the strings may not be next to each others when they'll be translated.

// Bad
<FormattedMessage
  defaultMessage="Do you want to {createSomething} in this list?"
  values={{
    createSomething: (
      <blink>
        <FormattedMessage defaultMessage="create something"/>
      </blink>
    )
  }}
/>

// Good
<FormattedMessage
  defaultMessage="Do you want to <blink>create something</blink> in this list?"
  values={{
    createSomething: function BlinkComponent(msg) {
      return (<blink>{msg}</blink>)
    }
  }}
/>

Use I18nFormatters to format rich text (bold, italic...etc)

import I18nFormatters from '../../I18nFormatters';

<FormattedMessage 
  id="_" 
  defaultMessage="This <strong>is</strong> <i>easier</i>." 
  values={I18nFormatters}
/>

Provide an ID when the translation depends on the context

In many Latin languages, the translation for a string like "Created at" will depend on the context because of the feminine/masculine forms. The best way to provide this context is to set an ID on the string:

<FormattedMessage
  id="expense.createdAt"
  defaultMessage"Created at"
/>

Translate links inline

In some parts of the code we translate links like this:

// Please don't do that!
<FormattedMessage
  defaultMessage="Please check our {documentationLink} to learn more!"
  values={{
    documentationLink: (
      <a href="https://docs.opencollective.com">
        <FormattedMessage id="documentation" defaultMessage="documentation" />
      </a>
    ),
  }}
/>

This is bad because we're creating two strings and translators loose the context when they translate one. You should do this instead:

import { getI18nLink } from './I18nFormatters';

// External link
<FormattedMessage
  defaultMessage="Please check our <link>documentation</link> to learn more!"
  values={{
    link: getI18nLink({ href: 'https://docs.opencollective.com' }),
  }}
/>

// Internal link
import Link from './Link';

<FormattedMessage
  defaultMessage="Please check <link>hosts page</link> to learn more!"
  values={{
    link: getI18nLink({ as: Link, route: 'hosts' }),
  }}
/>

FormattedMessage

The FormattedMessage component is the main way to translate strings. To use it, you just need to add the following import:

import { FormattedMessage } from 'react-intl'

Then you just add the component with an unique id and a defaultMessage.

For VSCode users, you can use the following snippet to make your life easier:

{
  "Formatted Message (react-intl)": {
    "scope": "javascript",
    "prefix": "formatted-message",
    "body": "<FormattedMessage id=\"$TM_FILENAME_BASE.$0\" defaultMessage=\"$1\"/>",
    "description": "Put the given string in a FormattedMessage"
  }
}

Add a new language

Add the language on Crowdin

Just go to https://crowdin.com/project/opencollective/settings#translations, click on Target languages pick the language and click Update.

Activate the language in the code

To activate a language on the website, we usually wait to have a correct translated ratio (20-30%). Then activate it by adding a new line in https://github.com/opencollective/opencollective-frontend/blob/main/lib/constants/locales.js.